Main Engine

Operator-grade admin panel for BookingAdvisor.

Routing rules are live, and the shell is ready to grow into reseller, provider, and access administration.

01 Routing control

Drive provider selection by reseller, service, and destination context.

02 Reseller context

Keep every action tied to a selected reseller across the admin workspace.

03 Growth-ready shell

Extend the same surface into more admin modules without touching the engine UI.

BA

BookingAdvisor

Sign in

Use your system credentials to enter the standalone admin workspace.

API Documentation

POST /api/v1/Auth/login

Authenticate with email and password to receive a JWT bearer token.

Request

Content-Type: application/json

FieldTypeConstraints
emailstringRequired. Valid email format. Max 255 characters.
passwordstringRequired. Minimum 6 characters.
{
  "email": "admin@bookingadvisor.com",
  "password": "P@ssw0rd"
}

Success Response 200

{
  "isSuccess": true,
  "message": "Login successful",
  "errors": {},
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "email": "admin@bookingadvisor.com",
    "name": "Admin User",
    "role": "Admin",
    "expiresAt": "2026-07-09T16:35:00Z"
  }
}

Error Responses

All errors use the Result envelope: {"isSuccess":false, "message":"...", "errors":{"key":["..."]}, "data":null}

Source: BookingAdvisor.Application/Common/Models/Result.cs

ScenarioStatusmessageerrors
Email not found401Invalid credentials{"general":["Email or password is incorrect"]}
Wrong password401Invalid credentials{"general":["Email or password is incorrect"]}
Email missing / empty400Validation failed{"email":["Email is required"]}
Email not a valid format400Validation failed{"email":["Invalid email format"]}
Email exceeds 255 chars400Validation failed{"email":["Email must not exceed 255 characters"]}
Password missing / empty400Validation failed{"password":["Password is required"]}
Password < 6 characters400Validation failed{"password":["Password must be at least 6 characters"]}
Unexpected server error401Login failed{"general":[""]}

Client Behaviour

  • On success: dataOf(result).token is saved to sessionStorage and the admin shell renders
  • On failure: body.message (or joined Object.values(body.errors)) displays in #loginError - see api() in app.js line 57-58
  • Token is sent as Authorization: Bearer <token> on every subsequent API call
  • Sign-out clears sessionStorage and resets the UI

Backend Call Chain

  1. AuthController.Login() - maps LoginRequest to LoginCommand
  2. ValidationBehavior - runs LoginCommandValidator (FluentValidation), groups failures by camelCase property name
  3. LoginCommandHandler.Handle()
    • _unitOfWork.Users.FirstOrDefaultAsync(u => u.Email == request.Email)
    • _passwordHasher.VerifyPassword(request.Password, user.PasswordHash)
    • _jwtTokenGenerator.GenerateToken(user)
BA

Main Engine

Admin panel

BookingAdvisor

Overview

Control Center

One place for reseller operations

This admin panel can grow beyond routing rules. For now, the routing module is live and the rest of the sections are prepared as admin modules.

Live module Routing Rules

Resellers

0 Available in this admin session

Providers

0 Assigned under the active reseller

Routing Rules

0 Saved rules for the active reseller

Backends

0 Engine connection records available to resellers

Mapping health

Catalogue coverage

Provider records currently usable in search, and the records still waiting for a mapping decision.

Provider Kind In use Needs review Unmapped Rejected Total Coverage
Loading…

Authored rule - changes results

Routing rules

Allow or block providers per reseller, service type, and travel target. These rules directly change which suppliers a customer's search reaches.

Monitoring - changes nothing

Coverage expectations

Declare that an airline should come from a supplier, and get alerted if it stops. Observation only - never changes what a customer sees.

Available now

Flight APIs

Document flight location search, flight search, and itinerary validation.

Available now

Reseller administration

A dedicated area for reseller details, assignments, and operational flags.

Available now

Provider administration

A clean place for provider activation, categories, and account health visibility.

Available now

Backend administration

Manage backend records that can be assigned to resellers.

Available now

Enum catalog

Inspect enum definitions and provider credential requirements.

Available now

Access management

Roles, permissions, and which admin-panel users hold them. Search and booking endpoints are never gated here.

Search workflow

Flight API documentation

Documented requests for flight location lookup, multi-provider flight search, and flight validation.

API Documentation

Flight search workflow

All flight endpoints require X-Reseller-Id: {resellerGuid}. Search and validation also require Authorization: Bearer <clientToken>.

Request Index

ActionMethodEndpointRequired headers
Search flight locationsGET/api/v1/flights/flight-locations?query={term}&limit={limit}X-Reseller-Id
Search flightsPOST/api/v1/flights/flightsX-Reseller-Id, Authorization
Validate flightPOST/api/v1/flights/validate-flightX-Reseller-Id, Authorization

Headers

X-Reseller-Id: 7a73b1a5-91de-4c44-9421-1a5f4db1d321
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
  • X-Reseller-Id is validated by RequireResellerIdAttribute.
  • Authorization is validated by RequireClientTokenAttribute on search and validation requests.

GET Location Search

Used before flight search to find airport/city tokens for departureId and arrivalId.

GET /api/v1/flights/flight-locations?query=dubai&limit=10
Query paramTypeNotes
querystringSearch text for airport, city, or code.
limitnumberOptional. Defaults to 10; routing autocomplete uses 30.
{
  "isSuccess": true,
  "message": "Found 2 locations from 1 providers",
  "errors": {},
  "data": {
    "results": [
      {
        "locationCode": "DXB",
        "locationName": "Dubai International",
        "cityCode": "DXB",
        "cityName": "Dubai",
        "countryName": "United Arab Emirates",
        "type": "Airport"
      }
    ],
    "totalCount": 1,
    "successfulProviders": 1,
    "failedProviders": 0,
    "executionTimeMs": 184
  }
}

POST Flight Search Request

Initial search omits searchCode and cursor. Follow-up paging/cache requests may send the returned values.

{
  "tripType": "RoundTrip",
  "segments": [
    {
      "departureId": "BGW",
      "arrivalId": "DXB",
      "fromDate": "2026-08-20T00:00:00"
    },
    {
      "departureId": "DXB",
      "arrivalId": "BGW",
      "fromDate": "2026-08-27T00:00:00"
    }
  ],
  "adults": 1,
  "children": 0,
  "infants": 0,
  "cabinClass": "Economy",
  "directFlightsOnly": false,
  "flexibleDays": 0,
  "pageSize": 20,
  "currencyCode": "USD"
}

Flight Search Fields

FieldTypeValidation
tripTypeenum/stringOneWay, RoundTrip, or MultiDestination.
segmentsarrayOneWay requires 1 segment, RoundTrip requires 2, MultiDestination requires at least 2.
departureId, arrivalIdstringRequired and cannot be equal.
fromDatedateRequired. Cannot be in the past.
adultsnumberRequired. 1 to 9.
children, infantsnumberOptional. Cannot be negative; infants cannot exceed adults.
cabinClassenum/stringEconomy, PremiumEconomy, Business, or First.
pageSizenumberRequired by validator. 1 to 100.
currencyCodestringOptional. Must be 3 characters when supplied.
flexibleDaysnumberOptional. 0 to 7.

Flight Search Success 200

{
  "isSuccess": true,
  "message": "Success",
  "errors": {},
  "data": {
    "searchCode": "FS-20260711-9f4f",
    "isSearchComplete": true,
    "nextCursor": null,
    "hasMore": false,
    "totalAvailable": 1,
    "successfulProviders": ["Trip Provider"],
    "failedProviders": [],
    "failures": {},
    "totalFlightCount": 1,
    "searchTtlSeconds": 900,
    "flights": [
      {
        "itineraryToken": "eyJzZWFyY2hDb2RlIjoiRlMtMjAyNjA3MTEtOWY0ZiJ9...",
        "provider": {
          "id": "35cc770d-d584-4ae7-8412-651425d6d335",
          "name": "Trip Provider",
          "type": "Trip"
        },
        "price": {
          "total": 420.50,
          "baseFare": 360.00,
          "taxesAndFees": 60.50,
          "currency": "USD"
        },
        "routes": [],
        "totalStops": 0,
        "totalDurationMinutes": 135,
        "cabinClass": "Economy",
        "isRefundable": false,
        "availableSeats": 4
      }
    ]
  }
}

POST Flight Validation Request

Use the itineraryToken from a selected search result. The server decodes it into search code, itinerary code, combination index, and provider ID.

{
  "itineraryToken": "eyJzZWFyY2hDb2RlIjoiRlMtMjAyNjA3MTEtOWY0ZiJ9..."
}

Flight Validation Success 200

{
  "isSuccess": true,
  "message": "Retrieved flight details from Trip Provider",
  "errors": {},
  "data": {
    "executionTimeMs": 312,
    "providerName": "Trip Provider",
    "itineraryToken": "eyJzZWFyY2hDb2RlIjoiRlMtMjAyNjA3MTEtOWY0ZiJ9...",
    "itineraryCode": "ITN-001",
    "searchCode": "FS-20260711-9f4f",
    "flightDetails": {
      "pricing": {
        "totalPrice": 420.50,
        "baseFare": 360.00,
        "serviceFee": 0,
        "currency": "USD",
        "isRefundable": false,
        "upsellSupport": true,
        "passengerFares": []
      },
      "routes": [],
      "fare": { "rules": [] },
      "requirements": {
        "contactRequired": true,
        "documentRequired": true,
        "birthdateRequired": true,
        "frequentFlyerAllowed": false,
        "acceptedDocumentTypes": ["Passport"],
        "birthdateRequiredByPTC": {},
        "contactRequiredByPTC": {}
      },
      "optionalServices": [],
      "payment": {
        "methods": ["Card"],
        "paymentBeforeDays": 0
      }
    }
  }
}

Validation and Errors

ScenarioStatusMessage
Missing reseller header400Missing X-Reseller-Id header. This header must contain the reseller ID.
Invalid reseller header400Invalid X-Reseller-Id header value. Expected a valid GUID...
Missing bearer token on search/validation401Missing or invalid Authorization header. Expected format: Bearer {token}
Invalid itinerary token400Invalid itinerary token
Invalid search request400FluentValidation errors grouped in the standard error envelope.

Client Workflow

  1. Call GET /flights/flight-locations to let the user choose departure and arrival values.
  2. Call POST /flights/flights with trip, segment, passenger, cabin, and paging fields.
  3. Render returned data.flights and store each itineraryToken.
  4. Call POST /flights/validate-flight with the selected token before booking or upsell flows.

Engine connections

Backend administration

Create backend records, update their descriptions, or remove records no longer used by resellers.

Backend details

New backend

Name Description Created Updated
Loading backends...

API Documentation

Backend CRUD requests

Backends are protected by Authorization: Bearer <token> and require the SuperAdmin role on the API.

Request Index

ActionMethodEndpointTrigger
List backendsGET/api/v1/backends?skip=0&take=1000After login and after create/update/delete.
Get backendGET/api/v1/backends/{id}Documented controller request; table edits use the loaded list row.
Create backendPOST/api/v1/backendsSubmitting the backend form in create mode.
Update backendPUT/api/v1/backends/{id}Submitting the backend form in edit mode.
Delete backendDELETE/api/v1/backends/{id}Delete button in the backend table after confirmation.
Refresh resellersGET/api/v1/resellers?skip=0&take=1000After backend changes so reseller backend names stay current.

Create Request

{
  "name": "Main Backend",
  "description": "Primary production backend for active resellers"
}

Update Request

{
  "id": "8149dc90-7554-4140-a961-6ce882242a14",
  "name": "Main Backend",
  "description": "Primary production backend"
}

Success Responses 200 201

{
  "isSuccess": true,
  "message": "Backend created successfully",
  "errors": {},
  "data": {
    "id": "8149dc90-7554-4140-a961-6ce882242a14",
    "name": "Main Backend",
    "description": "Primary production backend",
    "createdAt": "2026-07-11T09:20:00Z",
    "updatedAt": null
  }
}

Validation and Errors

ScenarioClient behaviorExpected API result
Name missingBrowser required validation blocks submission.No request is sent.
URL id and body id mismatchThe UI sends matching IDs from the selected row.400 with ID in URL does not match ID in request body.
Backend not foundToast or form error displays the backend message.404 from GET /backends/{id} or 400 for mutation handlers.
Unauthorized roleapi() throws the response message and shows it in the toast.401 or 403.

Client Behaviour

  • Create and update close the form, reload backends, refresh resellers, and show a toast.
  • Deleting a backend reloads backends and resellers so assignment names stay accurate.
  • The reseller page uses the same loaded backend list for its Backend select.

Markup / Commission

Channels

Platform-wide, one shared list of client surfaces a request can originate from (Web, Mobile App, Call Center, ...). Not reseller-scoped. A client declares its channel on the X-Channel request header, matched here by Code - never a required header.

Channel details

New channel

Code Name Description Active Created Updated
Loading channels...

API Documentation

Channel CRUD requests

Channels are protected by Authorization: Bearer <token> (channels.view/.create/.update/.delete permissions).

Request Index

ActionMethodEndpointTrigger
List channelsGET/api/v1/channelsAfter login and after create/update/delete.
Get channelGET/api/v1/channels/{id}Documented controller request; table edits use the loaded list row.
Create channelPOST/api/v1/channelsSubmitting the channel form in create mode.
Update channelPUT/api/v1/channels/{id}Submitting the channel form in edit mode.
Delete channelDELETE/api/v1/channels/{id}Delete button in the channel table after confirmation.

Create Request

{
  "code": "WEB",
  "name": "Website",
  "description": "Optional",
  "isActive": true
}

Markup / Commission

Sales groups

A saved shortcut for picking a curated set of resellers when authoring a Platform markup rule - resolved to plain reseller ids at save time. A rule never references a group directly, so editing or deleting a group here never changes an already-saved rule.

Sales group details

New sales group

Resellers

Title Resellers Active Created Updated
Loading sales groups...

API Documentation

Sales group CRUD requests

Protected by Authorization: Bearer <token> (sales-groups.view/.create/.update/.delete permissions).

Request Index

ActionMethodEndpointTrigger
List sales groupsGET/api/v1/sales-groupsAfter login and after create/update/delete.
Get sales groupGET/api/v1/sales-groups/{id}Documented controller request; table edits use the loaded list row.
Create sales groupPOST/api/v1/sales-groupsSubmitting the form in create mode.
Update sales groupPUT/api/v1/sales-groups/{id}Submitting the form in edit mode. Full-replace: the Resellers list sent is the group's new complete membership.
Delete sales groupDELETE/api/v1/sales-groups/{id}Delete button in the table after confirmation.

Create Request

{
  "title": "European resellers",
  "isActive": true,
  "resellerIds": ["..."]
}

Markup / Commission

Markup rules

Platform and reseller-authored rules are managed separately below. Both are evaluated by the same pricing engine, but each layer has its own priority scope.

Platform rule details

New markup rule

Platform rule Created and managed by the platform. It can apply to every reseller or selected resellers.

Providers

Leave every box unchecked to apply to every supplier.

Resellers

Leave every reseller unchecked to apply to all, or select specific resellers.

Criteria

Which criterion types are legal depends on the service type above (Flight: everything except Hotel; Hotel: Destination/Price range/Hotel; Package: Destination/Price range; ESim/Ride/Insurance: none - general only). Each criterion type gets its own fields below once selected, and every one of them accepts multiple values, the same way Origin/Destination let you pick several countries/cities/airports: date/amount/time ranges let you add several ranges ("+ Add range" - matches if ANY range fits), Fare family picks from the real fare-brand catalog same as Airline does, Booking class/Hotel accept several ids typed one at a time (press Enter to add - Hotel has no admin catalog search yet), and Cabin class/Refundability/Stops/Geography/Trip type are still checkboxes (there's no catalog for those - just a small fixed list) but styled as the same kind of multi-value pill.

Per-passenger

Flight and Package only. These are additive - a $20 main amount + $10/adult on 2 adults totals $40, not $20 or $20 alone.

Ancillary markup

Platform layer

Platform markup rules

Platform-owned rules. Each rule may cover every reseller or a selected set.

TitleServiceReseller scopePriorityAmountCriteriaStatusVersion
Loading platform rules...

Reseller layer

Reseller markup rules

Reseller-authored rules. Each rule belongs to exactly one reseller.

TitleServiceResellerPriorityAmountCriteriaStatusVersion
Loading reseller rules...

API Documentation

Markup rule CRUD requests

Protected by Authorization: Bearer <token> (markups.view/.create/.update/.delete permissions). Platform rules use /platform-markups; reseller-authored rules use /reseller-markups. Each controller fixes the layer server-side.

Request Index

ActionMethodEndpointTrigger
List platform rulesGET/api/v1/platform-markupsLoads the platform table.
Platform rule CRUDGET / POST / PUT / DELETE/api/v1/platform-markups/{id?}The platform controller always enforces Layer = Platform.
List reseller rulesGET/api/v1/reseller-markupsLoads the reseller table.
Reseller rule CRUDGET / POST / PUT / DELETE/api/v1/reseller-markups/{id?}The reseller controller always enforces Layer = Reseller. Updates cannot move rules between layers.

Create Request

{
  "title": "Platform Flight Base Markup",
  "serviceType": "Flight",
  "providerIds": [],
  "resellerIds": [],
  "direction": "Positive",
  "valueType": "FixedAmount",
  "amount": 20,
  "isActive": true,
  "ancillaryMarkupEnabled": false,
  "ancillaryAmount": null,
  "ancillaryValueType": null,
  "criteria": [],
  "isGeneral": true,
  "priority": 1000,
  "appliedOn": "TotalPrice",
  "perPassengerEnabled": false,
  "adultAmount": null, "adultValueType": null,
  "childAmount": null, "childValueType": null,
  "infantAmount": null, "infantValueType": null
}

Markup / Commission

Currencies & exchange rates

Manage the platform currency catalog and global USD-to-currency rates used by pricing. Each row means “1 USD equals X units of this currency”; services remain USD-only.

Currency details

New currency

Currency catalog

Available currencies

CodeNameSymbolSourceActive
Loading currencies...
Loading currencies...

Exchange-rate details

New exchange rate

Global exchange rates

USD conversion history

CurrencyUnits per USDEffectiveExpiresSource
Loading exchange rates...

Currency protection

New currency commission

Currency protection history

Currency commission history

CurrencyTypeValueEffectiveExpiresActive
Select a currency to view its currency commission history.

API Documentation

Currency and exchange-rate CRUD

The page uses the platform-wide endpoints /api/v1/currencies and /api/v1/currencies/exchange-rates. Every exchange-rate row stores the target currency ID and its units-per-USD rate; USD is the implicit source.

Request Index

ActionMethodEndpointTrigger
List currenciesGET/api/v1/currenciesLogin, refresh, and after currency mutations.
Currency CRUDPOST / PUT / DELETE/api/v1/currencies/{id?}Currency form and table action buttons.
List exchange ratesGET/api/v1/currencies/exchange-ratesLogin, refresh, and after rate mutations.
Exchange-rate historyPOST / DELETE/api/v1/currencies/exchange-rates/{id?}Create a new historical rate or delete a row; existing rates are immutable.
Currency commissionsGET / POST / DELETE/api/v1/currencies/commissions/{id?}Manage immutable USD currency-protection commission rows for the selected currency.
Currency commission historyGET/api/v1/currencies/commissions/historyView append-only commission change history for the selected currency.

System metadata

Enums catalog

Browse enum types, inspect enum values, and check required provider credential keys.

Type Name Value Description
Loading enum values…

Provider metadata

Required credential fields

Select a provider type.

API Documentation

Enum requests

The enum controller is read-only. These calls return metadata used by admin forms and provider credential validation.

Request Index

ActionMethodEndpointTrigger
List enum typesGET/api/v1/enums/typesOpening the app and clicking Refresh enums.
List all enum valuesGET/api/v1/enums?includeEmpty={true|false}Opening the Enums view with no type selected, or clicking Refresh enums.
List one enum type's valuesGET/api/v1/enums/{enumType}?includeEmpty={true|false}Choosing a specific enum type, or toggling Include empty option.
List provider credential keysGET/api/v1/enums/provider-credentials/{providerType}Clicking Load fields in the credentials panel.

Enum Types Response 200

{
  "isSuccess": true,
  "data": [
    { "name": "CabinClass", "value": 1, "description": null },
    { "name": "TripType", "value": 2, "description": null },
    { "name": "ProviderType", "value": 3, "description": null }
  ],
  "message": "Retrieved 8 enum types"
}

Enum Values Response 200

{
  "isSuccess": true,
  "data": {
    "enumType": "ProviderType",
    "items": [
      { "name": "Trip", "value": 0, "description": null },
      { "name": "MontyEsim", "value": 5, "description": null }
    ]
  },
  "message": "Retrieved 2 values for ProviderType"
}

All Enum Types Response (no enumType given) 200

{
  "isSuccess": true,
  "data": {
    "CabinClass": [
      { "name": "Economy", "value": 1, "description": null }
    ],
    "TripType": [
      { "name": "OneWay", "value": 1, "description": null }
    ],
    "ProviderType": [
      { "name": "Trip", "value": 0, "description": null }
    ]
  },
  "message": "Retrieved 8 enum types"
}

Provider Credentials Response 200

[
  "SecretKey",
  "ApplicationId",
  "MarkOrderAsPaidRequest"
]

Available Enum Types

ValueNameNotes
1CabinClassFlight cabin options.
2TripTypeOne-way, round-trip, and multi-destination.
3ProviderTypeProvider integration types.
4ProviderStatusProvider activation state.
5ProviderCategoryService category bit flags.
6ResellerStatusReseller activation state.
7ProviderResellerStatusProvider assignment activation state.
8UserRoleSuperAdmin, Admin, User.

Validation and Errors

ScenarioClient behaviorExpected API result
Invalid enum typeToast displays the backend message.400 with Invalid enum type.
No enum selectedTable shows every enum type's values with a Type column.200 with all enum types grouped by name.
Provider type without configured credentialsPanel displays No credential fields are required.200 with an empty array.

Provider routing rules

An authored rule - it directly changes which suppliers a customer's search reaches. Allow rules scope a search to the named providers first; every other eligible provider is still tried automatically if that returns nothing. Block rules exclude a provider absolutely. Naming no resellers targets every reseller. This is different from Coverage expectations, which only watches and alerts - it never changes a result.

Engine settings

Global engine settings. The service-offer cache TTL applies to Package, eSIM, Insurance, and Ride offer records.

Off (default): call only the preferred supplier(s) first; call everyone else only if that comes back empty - cheaper, respects paid/limited suppliers, but the customer waits for the preferred supplier's full response before the fallback even starts. On: call everyone at once - faster worst case, but defeats the point of scoping to a supplier that's paid or limited. Applies to Package, eSIM, Insurance, and Ride offer caches. The maximum is 1800 seconds (30 minutes).

New rules

Build one or more rules for flights, hotels, eSIM, or insurance. The active reseller in the top bar only supplies location search context for the From/To fields below - it does not limit which resellers a rule targets.

0 drafts
Add a rule to get started.

Existing rules

Rules for the active reseller, plus every rule that targets all resellers. Select "All resellers" in the top bar to see everything.

Priority Action Service Providers Resellers From Destination Status Notes
Loading...

API Documentation

Routing rules requests

All routing calls require Authorization: Bearer <token>. Location lookup calls also send X-Reseller-Id, used only to give the autocomplete a credentialed provider context - the saved rule itself is not scoped to that reseller unless it is explicitly checked below.

Request Index

ActionMethodEndpointTrigger
List rulesGET/api/v1/provider-routing-rules?resellerId={resellerId}Changing the active reseller or saving/deleting rules. Rules with no reseller listed always match (they target every reseller).
Create rulesPOST/api/v1/provider-routing-rules/batchSave all rules button.
Delete ruleDELETE/api/v1/provider-routing-rules/{id}Delete button in the existing-rules table.
Search flight locationsGET/api/v1/flights/flight-locations?query={term}&limit=30Typing in From/To fields for Flight or Insurance rules.
Search hotel locationsGET/api/v1/hotels/locations?query={term}&limit=30Typing in destination for Hotel rules.
List eSIM countriesGET/api/v1/esim/countriesAdding or editing an ESim draft rule.

POST Batch Request

Content-Type: application/json. providerIds is required, at least one. resellerIds may be empty - that means the rule targets every reseller.

{
  "rules": [
    {
      "resellerIds": ["7a73b1a5-91de-4c44-9421-1a5f4db1d321"],
      "providerIds": ["35cc770d-d584-4ae7-8412-651425d6d335", "1e088238-1c28-4ad3-b40a-3767f474063e"],
      "serviceType": "Flight",
      "action": "Allow",
      "priority": 1000,
      "departureLocation": "BGW",
      "destinationLocation": "DXB",
      "isEnabled": true,
      "notes": "Prefer these two for BGW-DXB"
    },
    {
      "resellerIds": [],
      "providerIds": ["1e088238-1c28-4ad3-b40a-3767f474063e"],
      "serviceType": "Hotel",
      "action": "Block",
      "priority": 2000,
      "departureLocation": null,
      "destinationLocation": "DXB",
      "isEnabled": true,
      "notes": "Never this supplier for Dubai hotels, any reseller"
    }
  ]
}

Success Responses 200

{
  "isSuccess": true,
  "message": "Rules saved",
  "errors": {},
  "data": [
    {
      "id": "0ef63ad3-8f2b-45e7-b815-2de0d6b044c7",
      "resellerIds": ["7a73b1a5-91de-4c44-9421-1a5f4db1d321"],
      "providerIds": ["35cc770d-d584-4ae7-8412-651425d6d335"],
      "serviceType": "Flight",
      "action": "Allow",
      "priority": 1000,
      "departureLocation": "BGW",
      "destinationLocation": "DXB",
      "isEnabled": true,
      "notes": null
    }
  ]
}
{
  "isSuccess": true,
  "data": {
    "results": [
      {
        "locationCode": "DXB",
        "locationName": "Dubai International",
        "cityCode": "DXB",
        "cityName": "Dubai",
        "countryName": "United Arab Emirates",
        "type": "Airport"
      }
    ]
  }
}

Validation and Errors

ScenarioClient behaviorExpected API result
No provider checkedShows Complete every rule.No batch request is sent.
Incomplete draftShows Complete every rule.No batch request is sent.
Provider not assigned to a checked resellerapi() throws the response message and shows it in the toast.400 with the standard error envelope.
Unauthorizedapi() throws the response message and shows it in the toast.401 or 403 with the standard error envelope.
Lookup failureAutocomplete remains open only when results are available; errors are shown in the toast.Standard error envelope.

Client Behaviour

  • Saved rules clear the draft list, reload existing rules, and update the Overview rule count.
  • Flight and Insurance rules require both departureLocation and destinationLocation.
  • Hotel and ESim rules send departureLocation: null.
  • Provider checkboxes are filtered to providers whose categories include the draft's service type; they are not filtered by reseller assignment client-side - an invalid combination is reported by the API and shown as a toast.
  • Leaving every reseller checkbox unchecked saves the rule with resellerIds: [], meaning it targets all resellers.
  • Country results are cached per selected reseller in state.countries.

Supplier coverage expectations

"Turkish Airlines must be available from Amadeus and Sabre" - a monitoring declaration, not a routing rule. It never changes what a customer sees; it only raises an alert when the expected content stops arriving. The monitor that evaluates these is a later piece of work - this screen is storage and CRUD only.

New expectation

Airlines and providers are both required lists - "TK from Amadeus and Sabre" is one expectation with two providers checked, not two expectations.

Airlines

Providers expected to carry these airlines

Origin - leave as "Any route" to apply to every route

Destination - leave as "Any route" to apply to every route

Counts consecutive searches where none of the airlines appear from any of the providers above. Any search where they do appear resets the count to zero.
Set at least one cooldown field. After firing, the check goes quiet for the cooldown period, then re-checks - if the airline is still missing, it fires again.
Title Airlines Providers Route Misses Cooldown Status Streak
Loading...

API Documentation

Coverage expectation requests

Super-admin only, same bearer token as every other admin call.

Request Index

ActionMethodEndpoint
ListGET/api/v1/supplier-coverage-expectations
CreatePOST/api/v1/supplier-coverage-expectations
UpdatePUT/api/v1/supplier-coverage-expectations/{id}
DeleteDELETE/api/v1/supplier-coverage-expectations/{id}

Detection - not built yet

Consecutive-miss streak: each search where none of the listed airlines came from any of the listed providers increments a counter; any search where they do appear resets it to zero. Hitting consecutiveMissThreshold fires a notification to the holders of the technical-notification permission. Apply the cooldown. When the cooldown expires, re-check - if still missing, fire again. This screen only stores the declaration; nothing evaluates it yet.

Diagnostics

Search execution log

Which providers were actually called on each real search, which stage they ran in, and whether they succeeded. Written asynchronously after the search completes - a row can lag a few seconds behind the search itself. Purely diagnostic: nothing here affects what a customer sees.

Defaults to the last 7 days when no date range is set. Reseller filter uses the picker in the top bar - leave it on "All resellers" to see every reseller's searches.

When Service Stage Route Provider Provider stage Outcome Results Error
Loading…

API Documentation

Search execution log requests

Super-admin only, same bearer token as every other admin call. Read-only - rows are written by a background consumer, never through this endpoint.

Request Index

ActionMethodEndpoint
ListGET/api/v1/search-execution-records?resellerId=&serviceType=&stage=&outcome=&from=&to=&page=&pageSize=

Reading the Stage column

ScopedOnly: an Allow rule scoped this search and stage 1 found something. PublicOnly: no Allow rule matched at all, so every eligible provider was searched directly - this is normal, not a failure signal. PublicFallback: an Allow rule scoped this search, stage 1 ran and found nothing usable, so the unrestricted stage 2 also ran. Parallel: the engine setting to run both stages together was on.

Logs

Audit logs

Every request the engine received, every call it made to a supplier, and every change it saved to the database. Secrets are blanked out.

With no "From" date, the last 7 days are shown (the last hour when searching for text inside events, because that search reads every event in full; events over 1 MB are skipped by it). Times are shown in your local time. The user is recorded only for events written after this page was added.

WhenEventStatusURLms ResellerUserIP
Loading…

Event details


          

API Documentation

Audit log requests

Read-only. Needs the logs.view permission.

ActionMethodEndpoint
ListGET/api/v1/logs/audit?from=&to=&resellerId=&user=&category=&eventType=&method=&status=&url=&correlationId=&ip=&minDurationMs=&text=&includeOptions=&page=&pageSize=
One event, in fullGET/api/v1/logs/audit/{id}

Logs

Application logs

What the engine itself wrote while running: information, warnings, errors and their exceptions. Click a row to see the exception and the details.

With no "From" date, the last 7 days are shown. Times are shown in your local time. Application logs are not tied to a user; only some rows carry a reseller.

WhenLevelSourceMessageReseller
Loading…

API Documentation

Application log requests

Read-only. Needs the logs.view permission.

ActionMethodEndpoint
ListGET/api/v1/logs/application?from=&to=&level=&message=&source=&resellerId=&exception=&onlyExceptions=&page=&pageSize=

Bookings

Orders

The engine's own record of each booking - exactly what was sent to the supplier at create time, and its status after reservation. Read-only: rows are written by the create/reserve flow itself, never through this screen.

Loading…

Order

Order

Every field the engine captured when it created this order, split out by item - passengers, rooms, occupants and the rest - rather than one flattened blob.

Owner

Status history

When From To Changed by Reason
No history yet.

API Documentation

Order requests

Super-admin only, same bearer token as every other admin call. Read-only - rows are written by the create-order/reserve flow, never through this endpoint.

Request Index

ActionMethodEndpoint
ListGET/api/v1/engine-orders?resellerId=&providerId=&itemType=&status=&externalOrderId=&page=&pageSize=
Detail (includes status history)GET/api/v1/engine-orders/{id}

Tenant management

Reseller administration

Create resellers, update their engine connection and status, or remove obsolete records.

Reseller details

New reseller

Off (default): the PlatformMarkup amount is computed and applied internally but not disclosed in the response. Does not affect what's persisted - the calling backend always receives and records the real amount regardless of this flag. Off (default): the currency commission is applied to the final USD price but its amount is omitted from the breakdown. Off (default): the original supplier amount, source currency, USD rate, and converted USD amount are omitted. Off (default): only the first leg (outbound) is checked against a rule's Origin/Destination criterion for this reseller's requests. On: either leg (outbound or return) satisfying the route is enough. Governs both the Platform and Reseller markup layers' evaluation of this reseller's own requests. Same as the round-trip toggle above, for multi-city itineraries instead.

Provider assignments

Choose the providers and service categories available to this reseller.

0 assigned

Loading providers...

Name Backend Providers Status Created
Loading resellers...

API Documentation

Reseller CRUD requests

All reseller requests require Authorization: Bearer <token> and use the standard Result<T> response envelope.

Request Index

ActionMethodEndpointTrigger
List resellersGET/api/v1/resellers?skip=0&take=1000After login, after create/update/delete, and when provider data changes.
Create resellerPOST/api/v1/resellersSubmitting the reseller form in create mode.
Update resellerPUT/api/v1/resellers/{id}Submitting the reseller form in edit mode.
Delete resellerDELETE/api/v1/resellers/{id}Delete button in the reseller table after confirmation.
List backendsGET/api/v1/backends?skip=0&take=1000After login to populate the Backend select.
List providersGET/api/v1/providers?skip=0&take=1000After login to build provider assignment controls.

Create Request

{
  "name": "Acme Travel",
  "status": "Active",
  "backendId": "8149dc90-7554-4140-a961-6ce882242a14",
  "providers": [
    {
      "providerId": "35cc770d-d584-4ae7-8412-651425d6d335",
      "status": "Active",
      "assignedCategories": "Flight, Hotel",
      "extraInfo": "{\"market\":\"IQ\"}"
    }
  ]
}

Update Request

{
  "id": "7a73b1a5-91de-4c44-9421-1a5f4db1d321",
  "name": "Acme Travel",
  "status": "Inactive",
  "backendId": null,
  "providers": [
    {
      "providerId": "35cc770d-d584-4ae7-8412-651425d6d335",
      "status": "Active",
      "assignedCategories": "Flight",
      "extraInfo": ""
    }
  ]
}

Success Response 200

{
  "isSuccess": true,
  "message": "Reseller saved",
  "errors": {},
  "data": {
    "id": "7a73b1a5-91de-4c44-9421-1a5f4db1d321",
    "name": "Acme Travel",
    "backendId": "8149dc90-7554-4140-a961-6ce882242a14",
    "backendName": "Main Backend",
    "status": "Active",
    "createdAt": "2026-07-11T09:20:00Z",
    "providers": [
      {
        "providerId": "35cc770d-d584-4ae7-8412-651425d6d335",
        "providerName": "Trip Provider",
        "status": "Active",
        "assignedCategories": "Flight, Hotel"
      }
    ]
  }
}

Validation and Errors

ScenarioClient behaviorExpected API result
Name missingBrowser required validation blocks submission.No request is sent.
Assigned provider has no categoriesShows Select at least one category for every assigned provider.No request is sent.
Delete cancelledTable remains unchanged.No request is sent.
API validation failureresellerFormError displays the backend message.400 standard error envelope.

Client Behaviour

  • Create and update close the form, reload resellers, and show a toast.
  • Deleting the active reseller clears the global reseller selector.
  • Provider assignments are rebuilt from the latest provider list each time the form opens.
  • backendId is sent as null when No backend is selected.

Supplier management

Provider administration

Create suppliers, update service categories, credentials, status, and carrier display behavior.

Provider details

New provider

Search access

A supplier flagged here with no routing rule naming it is never searched at all.

Service categories

Select every service this supplier supports.

Issuance modes

Which booking modes this supplier is allowed to use.

Credentials

The fields this provider type needs. Saved values are never shown: when editing, fill in only the fields you want to change.

Name Type Categories Carrier Status
Loading providers...

Trip static catalogue

Test Trip static data

Page 1 of 1
View raw Trip response

            

API Documentation

Provider CRUD requests

All provider requests require Authorization: Bearer <token>. The UI sends enum values as numbers and category selections as a bitmask.

Request Index

ActionMethodEndpointTrigger
List providersGET/api/v1/providers?skip=0&take=1000After login and after create/update/delete/status changes.
Create providerPOST/api/v1/providersSubmitting the provider form in create mode.
Update providerPUT/api/v1/providers/{id}Submitting the provider form in edit mode.
Delete providerDELETE/api/v1/providers/{id}Delete button in the provider table after confirmation.
Change provider statusPATCH/api/v1/providers/{id}/statusActivate/Deactivate button in the provider table.
Test country catalogueGET/api/v1/providers/{id}/static-data/countries?page=1Test Trip countries. Uses only the provider credentials JSON.
Test city catalogueGET/api/v1/providers/{id}/static-data/cities?page=1Test Trip cities. Uses only the provider credentials JSON and follows Trip pagination.
Refresh resellersGET/api/v1/resellers?skip=0&take=1000After provider changes so assignments and stats stay current.

Create Request

FieldTypeNotes
typenumber0 Trip, 5 MontyEsim, 10 Viator, 15 GooglePlaces, 16 ElifeRide, 17 TuneProtect, 20 Telegram.
statusnumber0 Active, 1 Inactive.
carrierDisplaynumber0 Marketing, 1 Operating.
categoriesnumberBitmask: Flight 1, Hotel 2, Package 4, ESim 8, Visa 16, Sport 32, DynamicPackage 64, Activity 128, Places 256, Ride 512, Insurance 1024.
{
  "name": "Trip Provider",
  "status": 0,
  "type": 0,
  "carrierDisplay": 0,
  "categories": 3,
  "url": "https://supplier.example.com/api",
  "credentials": "{\"ApplicationId\":\"app-id\",\"ApplicationSecret\":\"secret\"}"
}

Update and Status Requests

{
  "id": "35cc770d-d584-4ae7-8412-651425d6d335",
  "name": "Trip Provider",
  "status": 0,
  "type": 0,
  "carrierDisplay": 1,
  "categories": 1027,
  "url": "https://supplier.example.com/api",
  "credentials": ""
}
{
  "id": "35cc770d-d584-4ae7-8412-651425d6d335",
  "status": 1
}

Success Response 200

{
  "isSuccess": true,
  "message": "Provider saved",
  "errors": {},
  "data": {
    "id": "35cc770d-d584-4ae7-8412-651425d6d335",
    "name": "Trip Provider",
    "type": "Trip",
    "status": "Active",
    "carrierDisplay": "Marketing",
    "categories": "Flight, Hotel",
    "url": "https://supplier.example.com/api"
  }
}

Validation and Errors

ScenarioClient behaviorExpected API result
Name or URL missingBrowser required validation blocks submission.No request is sent.
No service category selectedShows Select at least one service category.No request is sent.
Required credential field left emptyShows Fill in the required credentials: ...No request is sent.
API validation failureproviderFormError displays the backend message.400 standard error envelope.

Client Behaviour

  • Create, update, delete, and status changes reload providers and resellers.
  • Credentials are entered as one input per key. The required keys come from GET /api/v1/enums/provider-credentials/{providerType}; the API never returns credential values, only the names of the keys that are set (credentialKeys). When editing, only the fields that are filled in are sent and only those keys change.
  • Status toggles send the next numeric status only: 0 for activate and 1 for deactivate.
  • Provider categories are displayed as names but submitted as the summed bitmask value.

Provider mapping

Mapping overview

How much of each supplier's catalogue we can actually search. Only auto-mapped and manually mapped records are used at runtime — anything waiting for review resolves to nothing, so a customer searching there gets an empty page.

Where things stand

Counts by supplier and kind

A large "needs review" beside a small "mapped" is the shape of a queue being produced and never worked. That is the state in which the matcher runs unsupervised and nothing it decides ever reaches a customer.

Provider Kind In use Needs review Unmapped Rejected Total Coverage
Loading…

Provider mapping

Review queue

Pairs the matcher was not confident enough to link on its own. Nothing else can release one — until somebody decides here, the record stays invisible to search.

Ordered by how much the place is searched, because a queue this size is never read to the end — whatever sits at the top is in practice the only thing anyone reviews.

Provider record Suggested match Confidence Distance Demand State
Loading…
Nothing selected.

Approve confirms existing suggestions. For hotels with no suggestion, “Create new hotels” re-checks each selected row before creating it, so two providers describing one hotel are routed together instead of becoming duplicates. Review actions are capped at 200 records; creation is capped at 50.

Side by side

Record

A missing link leaves a duplicate on screen, which is untidy. A wrong link shows one hotel's photos and then books a different property — so every signal the matcher used is printed here rather than summarised into a score.

This assigns the selected country to the new canonical city. It does not create or guess a country.

What the provider sent

What it is linked to

Alternatives

Scored fresh, on name and distance only. The matcher's own verdict is in the evidence below, and it used signals this list cannot show.

Our record Code Where Name score Distance Why
Loading…

What the matcher decided, and why

No evidence recorded.

Provider matching

Matching rules

Decide how sure the engine has to be before it treats two providers' records as the same thing. Changes apply to the next match run — records already linked are left alone.

These are still the starting values. Nobody has tuned them yet, so treat the results as a first guess rather than a measured setting.

1

Step 1

How sure is sure enough?

Every comparison produces a score from 0 to 100. These two numbers decide what happens to it.

Not a match0–80
A person decides80–95
Linked automatically95–100
2

Step 2

What counts, and how much

The score is these eight signals added together. The shares must total 100%. If one of the two records is missing a signal, its share is spread over the signals that are there — so a provider that sends less data is not punished for it.

100%
3

Step 3

Distance

How far apart two records can be and still be one place. The score fades gradually as the distance grows, reaching zero at the last band.

4

Step 4

Absolute limits

These beat the score. A pair can reach 99% and still be sent to a person if it crosses one of these — which is not the same as being rejected, it still waits in the queue.

Try it

See what these settings actually do

Nobody can tell whether 32% is the right share for names by looking at it. Take two records you already know the answer for, put them in, and see whether the rules agree with you. This uses the settings above as they are on screen — you do not have to save first.

Start from an example:

Record A — as one provider sends it

Record B — as another provider sends it

Provider catalogues

Catalogue sync

How often each supplier's country and city lists are re-read, and what the syncs are doing right now. The clock runs from the last completed pass — a large catalogue read in slices is not counted as done until it finishes.

1

Defaults

How often, for every provider

These cover every supplier that has no rule of its own. Switching a schedule off stops the background runs only — the manual buttons below still work.

2

Overrides

Rules for one supplier

A supplier with a tight rate limit, or a catalogue that barely moves, can have its own interval. Anything without an override follows the defaults above.

3

Right now

What each catalogue is doing

The interval shown is the one actually in force after defaults and overrides are resolved. "Sync now" ignores the interval and runs immediately.

Provider Catalogue Every Last completed Next due Progress
Loading…

Provider catalogues

Hotel catalogue

Reads a provider's own hotel list with its own credentials — no reseller, no client token — exactly like the country and city catalogues. This is where postcode, phone and email come from; hotel search sends none of them.

1

Which provider

Pick a supplier and narrow it down

A hotel catalogue is far too large to read end to end — one supplier lists over four thousand hotels in Dubai alone. It is always read a city at a time, which is why cities are synced first.

2

Response

What came back

The coverage bar is the useful part: a supplier that sends no postcodes or phone numbers cannot be matched on them, however the weights are set on the Matching rules screen.

View raw response

            

Admin module

Roles

Bundles of permissions assignable to admin-panel users. Only admin/management actions carry a permission - search and booking endpoints are never gated.

Role details

New role

Permissions

Name Description Status Permissions Users
Loading roles...

Admin module

Users

Admin-panel users and which roles each one holds. A user can hold any number of roles at once.

User details

New user

Roles

Name Email Legacy role Assigned roles Assigned resellers
Loading users...

Manage access

User roles

Manage access

User resellers

Only relevant when this user lacks the unrestricted role permission for a reseller-scoped action (e.g. Markups) - they then fall back to just these resellers. You can only assign a reseller you yourself have access to.

API Documentation

Roles & users requests

Each request below requires Authorization: Bearer <token> from an account holding its own specific permission (e.g. roles.create, users.delete) - not one blanket permission for the whole module. Every legacy SuperAdmin is granted all of them automatically.

Request Index

ActionMethodEndpointPermissionTrigger
List rolesGET/api/v1/rolesroles.viewAfter login and after create/update/delete.
Get roleGET/api/v1/roles/{id}roles.viewOpening a role in edit mode (loads its permission ids).
List permissionsGET/api/v1/roles/permissionsroles.viewAfter login, to populate the permission checkboxes.
Create rolePOST/api/v1/rolesroles.createSubmitting the role form in create mode.
Update rolePUT/api/v1/roles/{id}roles.updateSubmitting the role form in edit mode.
Delete roleDELETE/api/v1/roles/{id}roles.deleteDelete button in the role table after confirmation.
List usersGET/api/v1/usersusers.viewAfter login and after any create/delete/role change.
Create userPOST/api/v1/usersusers.createSubmitting the user form.
Delete userDELETE/api/v1/users/{id}users.deleteDelete button in the user table after confirmation. A user cannot delete their own account.
Assign rolePOST/api/v1/users/{id}/roles/{roleId}users.updateChecking a role checkbox in the "Manage access" panel.
Remove roleDELETE/api/v1/users/{id}/roles/{roleId}users.updateUnchecking a role checkbox in the "Manage access" panel.
Assign resellerPOST/api/v1/users/{id}/resellers/{resellerId}users.updateChecking a reseller checkbox in "Manage resellers" - rejected if the acting admin doesn't have access to that reseller themselves.
Remove resellerDELETE/api/v1/users/{id}/resellers/{resellerId}users.updateUnchecking a reseller checkbox in "Manage resellers".

Create User Request

{
  "name": "Jordan Lee",
  "email": "jordan@bookingadvisor.com",
  "password": "P@ssw0rd",
  "roleIds": ["8149dc90-7554-4140-a961-6ce882242a14"]
}

Create/Update Role Request

{
  "name": "Provider Managers",
  "description": "Can manage provider records and routing rules",
  "isActive": true,
  "permissionIds": ["8149dc90-7554-4140-a961-6ce882242a14"]
}

Role Success Response 200 201

{
  "isSuccess": true,
  "message": "Role created successfully",
  "errors": {},
  "data": {
    "id": "8149dc90-7554-4140-a961-6ce882242a14",
    "name": "Provider Managers",
    "description": "Can manage provider records and routing rules",
    "isActive": true,
    "permissionIds": ["8149dc90-7554-4140-a961-6ce882242a14"]
  }
}

User List Response 200

{
  "isSuccess": true,
  "message": "OK",
  "errors": {},
  "data": [
    {
      "id": "8149dc90-7554-4140-a961-6ce882242a14",
      "name": "Admin User",
      "email": "admin@bookingadvisor.com",
      "role": "SuperAdmin",
      "roleIds": ["a1b2c3d4-0000-0000-0000-000000000000"],
      "roleNames": ["Super Admin"]
    }
  ]
}