Drive provider selection by reseller, service, and destination context.
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.
Keep every action tied to a selected reseller across the admin workspace.
Extend the same surface into more admin modules without touching the engine UI.
API Documentation
POST /api/v1/Auth/login
Authenticate with email and password to receive a JWT bearer token.
Request
Content-Type: application/json
| Field | Type | Constraints |
|---|---|---|
email | string | Required. Valid email format. Max 255 characters. |
password | string | Required. 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
| Scenario | Status | message | errors |
|---|---|---|---|
| Email not found | 401 | Invalid credentials | {"general":["Email or password is incorrect"]} |
| Wrong password | 401 | Invalid credentials | {"general":["Email or password is incorrect"]} |
| Email missing / empty | 400 | Validation failed | {"email":["Email is required"]} |
| Email not a valid format | 400 | Validation failed | {"email":["Invalid email format"]} |
| Email exceeds 255 chars | 400 | Validation failed | {"email":["Email must not exceed 255 characters"]} |
| Password missing / empty | 400 | Validation failed | {"password":["Password is required"]} |
| Password < 6 characters | 400 | Validation failed | {"password":["Password must be at least 6 characters"]} |
| Unexpected server error | 401 | Login failed | {"general":[" |
Client Behaviour
- On success:
dataOf(result).tokenis saved tosessionStorageand the admin shell renders - On failure:
body.message(or joinedObject.values(body.errors)) displays in#loginError- seeapi()in app.js line 57-58 - Token is sent as
Authorization: Bearer <token>on every subsequent API call - Sign-out clears
sessionStorageand resets the UI
Backend Call Chain
AuthController.Login()- mapsLoginRequesttoLoginCommandValidationBehavior- runsLoginCommandValidator(FluentValidation), groups failures by camelCase property nameLoginCommandHandler.Handle()_unitOfWork.Users.FirstOrDefaultAsync(u => u.Email == request.Email)_passwordHasher.VerifyPassword(request.Password, user.PasswordHash)_jwtTokenGenerator.GenerateToken(user)
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.
Resellers
0 Available in this admin sessionProviders
0 Assigned under the active resellerRouting Rules
0 Saved rules for the active resellerBackends
0 Engine connection records available to resellersMapping 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
| Action | Method | Endpoint | Required headers |
|---|---|---|---|
| Search flight locations | GET | /api/v1/flights/flight-locations?query={term}&limit={limit} | X-Reseller-Id |
| Search flights | POST | /api/v1/flights/flights | X-Reseller-Id, Authorization |
| Validate flight | POST | /api/v1/flights/validate-flight | X-Reseller-Id, Authorization |
Headers
X-Reseller-Id: 7a73b1a5-91de-4c44-9421-1a5f4db1d321
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
X-Reseller-Idis validated byRequireResellerIdAttribute.Authorizationis validated byRequireClientTokenAttributeon 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 param | Type | Notes |
|---|---|---|
query | string | Search text for airport, city, or code. |
limit | number | Optional. 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
| Field | Type | Validation |
|---|---|---|
tripType | enum/string | OneWay, RoundTrip, or MultiDestination. |
segments | array | OneWay requires 1 segment, RoundTrip requires 2, MultiDestination requires at least 2. |
departureId, arrivalId | string | Required and cannot be equal. |
fromDate | date | Required. Cannot be in the past. |
adults | number | Required. 1 to 9. |
children, infants | number | Optional. Cannot be negative; infants cannot exceed adults. |
cabinClass | enum/string | Economy, PremiumEconomy, Business, or First. |
pageSize | number | Required by validator. 1 to 100. |
currencyCode | string | Optional. Must be 3 characters when supplied. |
flexibleDays | number | Optional. 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
| Scenario | Status | Message |
|---|---|---|
| Missing reseller header | 400 | Missing X-Reseller-Id header. This header must contain the reseller ID. |
| Invalid reseller header | 400 | Invalid X-Reseller-Id header value. Expected a valid GUID... |
| Missing bearer token on search/validation | 401 | Missing or invalid Authorization header. Expected format: Bearer {token} |
| Invalid itinerary token | 400 | Invalid itinerary token |
| Invalid search request | 400 | FluentValidation errors grouped in the standard error envelope. |
Client Workflow
- Call
GET /flights/flight-locationsto let the user choose departure and arrival values. - Call
POST /flights/flightswith trip, segment, passenger, cabin, and paging fields. - Render returned
data.flightsand store eachitineraryToken. - Call
POST /flights/validate-flightwith 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.
| 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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List backends | GET | /api/v1/backends?skip=0&take=1000 | After login and after create/update/delete. |
| Get backend | GET | /api/v1/backends/{id} | Documented controller request; table edits use the loaded list row. |
| Create backend | POST | /api/v1/backends | Submitting the backend form in create mode. |
| Update backend | PUT | /api/v1/backends/{id} | Submitting the backend form in edit mode. |
| Delete backend | DELETE | /api/v1/backends/{id} | Delete button in the backend table after confirmation. |
| Refresh resellers | GET | /api/v1/resellers?skip=0&take=1000 | After 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
| Scenario | Client behavior | Expected API result |
|---|---|---|
| Name missing | Browser required validation blocks submission. | No request is sent. |
| URL id and body id mismatch | The UI sends matching IDs from the selected row. | 400 with ID in URL does not match ID in request body. |
| Backend not found | Toast or form error displays the backend message. | 404 from GET /backends/{id} or 400 for mutation handlers. |
| Unauthorized role | api() 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.
| 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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List channels | GET | /api/v1/channels | After login and after create/update/delete. |
| Get channel | GET | /api/v1/channels/{id} | Documented controller request; table edits use the loaded list row. |
| Create channel | POST | /api/v1/channels | Submitting the channel form in create mode. |
| Update channel | PUT | /api/v1/channels/{id} | Submitting the channel form in edit mode. |
| Delete channel | DELETE | /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.
| 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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List sales groups | GET | /api/v1/sales-groups | After login and after create/update/delete. |
| Get sales group | GET | /api/v1/sales-groups/{id} | Documented controller request; table edits use the loaded list row. |
| Create sales group | POST | /api/v1/sales-groups | Submitting the form in create mode. |
| Update sales group | PUT | /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 group | DELETE | /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 layer
Platform markup rules
Platform-owned rules. Each rule may cover every reseller or a selected set.
| Title | Service | Reseller scope | Priority | Amount | Criteria | Status | Version | |
|---|---|---|---|---|---|---|---|---|
| Loading platform rules... | ||||||||
Reseller layer
Reseller markup rules
Reseller-authored rules. Each rule belongs to exactly one reseller.
| Title | Service | Reseller | Priority | Amount | Criteria | Status | Version | |
|---|---|---|---|---|---|---|---|---|
| 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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List platform rules | GET | /api/v1/platform-markups | Loads the platform table. |
| Platform rule CRUD | GET / POST / PUT / DELETE | /api/v1/platform-markups/{id?} | The platform controller always enforces Layer = Platform. |
| List reseller rules | GET | /api/v1/reseller-markups | Loads the reseller table. |
| Reseller rule CRUD | GET / 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 catalog
Available currencies
| Code | Name | Symbol | Source | Active | |
|---|---|---|---|---|---|
| Loading currencies... | |||||
Global exchange rates
USD conversion history
| Currency | Units per USD | Effective | Expires | Source | |
|---|---|---|---|---|---|
| Loading exchange rates... | |||||
Currency protection history
Currency commission history
| Currency | Type | Value | Effective | Expires | Active | |
|---|---|---|---|---|---|---|
| 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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List currencies | GET | /api/v1/currencies | Login, refresh, and after currency mutations. |
| Currency CRUD | POST / PUT / DELETE | /api/v1/currencies/{id?} | Currency form and table action buttons. |
| List exchange rates | GET | /api/v1/currencies/exchange-rates | Login, refresh, and after rate mutations. |
| Exchange-rate history | POST / DELETE | /api/v1/currencies/exchange-rates/{id?} | Create a new historical rate or delete a row; existing rates are immutable. |
| Currency commissions | GET / POST / DELETE | /api/v1/currencies/commissions/{id?} | Manage immutable USD currency-protection commission rows for the selected currency. |
| Currency commission history | GET | /api/v1/currencies/commissions/history | View 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
API Documentation
Enum requests
The enum controller is read-only. These calls return metadata used by admin forms and provider credential validation.
Request Index
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List enum types | GET | /api/v1/enums/types | Opening the app and clicking Refresh enums. |
| List all enum values | GET | /api/v1/enums?includeEmpty={true|false} | Opening the Enums view with no type selected, or clicking Refresh enums. |
| List one enum type's values | GET | /api/v1/enums/{enumType}?includeEmpty={true|false} | Choosing a specific enum type, or toggling Include empty option. |
| List provider credential keys | GET | /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
| Value | Name | Notes |
|---|---|---|
1 | CabinClass | Flight cabin options. |
2 | TripType | One-way, round-trip, and multi-destination. |
3 | ProviderType | Provider integration types. |
4 | ProviderStatus | Provider activation state. |
5 | ProviderCategory | Service category bit flags. |
6 | ResellerStatus | Reseller activation state. |
7 | ProviderResellerStatus | Provider assignment activation state. |
8 | UserRole | SuperAdmin, Admin, User. |
Validation and Errors
| Scenario | Client behavior | Expected API result |
|---|---|---|
| Invalid enum type | Toast displays the backend message. | 400 with Invalid enum type. |
| No enum selected | Table shows every enum type's values with a Type column. | 200 with all enum types grouped by name. |
| Provider type without configured credentials | Panel 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.
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.
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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List rules | GET | /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 rules | POST | /api/v1/provider-routing-rules/batch | Save all rules button. |
| Delete rule | DELETE | /api/v1/provider-routing-rules/{id} | Delete button in the existing-rules table. |
| Search flight locations | GET | /api/v1/flights/flight-locations?query={term}&limit=30 | Typing in From/To fields for Flight or Insurance rules. |
| Search hotel locations | GET | /api/v1/hotels/locations?query={term}&limit=30 | Typing in destination for Hotel rules. |
| List eSIM countries | GET | /api/v1/esim/countries | Adding 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
| Scenario | Client behavior | Expected API result |
|---|---|---|
| No provider checked | Shows Complete every rule. | No batch request is sent. |
| Incomplete draft | Shows Complete every rule. | No batch request is sent. |
| Provider not assigned to a checked reseller | api() throws the response message and shows it in the toast. | 400 with the standard error envelope. |
| Unauthorized | api() throws the response message and shows it in the toast. | 401 or 403 with the standard error envelope. |
| Lookup failure | Autocomplete 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
departureLocationanddestinationLocation. - 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.
| 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
| Action | Method | Endpoint |
|---|---|---|
| List | GET | /api/v1/supplier-coverage-expectations |
| Create | POST | /api/v1/supplier-coverage-expectations |
| Update | PUT | /api/v1/supplier-coverage-expectations/{id} |
| Delete | DELETE | /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
| Action | Method | Endpoint |
|---|---|---|
| List | GET | /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.
| When | Event | Status | URL | ms | Reseller | User | IP | |
|---|---|---|---|---|---|---|---|---|
| Loading… | ||||||||
Event details
API Documentation
Audit log requests
Read-only. Needs the logs.view permission.
| Action | Method | Endpoint |
|---|---|---|
| List | GET | /api/v1/logs/audit?from=&to=&resellerId=&user=&category=&eventType=&method=&status=&url=&correlationId=&ip=&minDurationMs=&text=&includeOptions=&page=&pageSize= |
| One event, in full | GET | /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.
| When | Level | Source | Message | Reseller |
|---|---|---|---|---|
| Loading… | ||||
API Documentation
Application log requests
Read-only. Needs the logs.view permission.
| Action | Method | Endpoint |
|---|---|---|
| List | GET | /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
| Action | Method | Endpoint |
|---|---|---|
| List | GET | /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.
| 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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List resellers | GET | /api/v1/resellers?skip=0&take=1000 | After login, after create/update/delete, and when provider data changes. |
| Create reseller | POST | /api/v1/resellers | Submitting the reseller form in create mode. |
| Update reseller | PUT | /api/v1/resellers/{id} | Submitting the reseller form in edit mode. |
| Delete reseller | DELETE | /api/v1/resellers/{id} | Delete button in the reseller table after confirmation. |
| List backends | GET | /api/v1/backends?skip=0&take=1000 | After login to populate the Backend select. |
| List providers | GET | /api/v1/providers?skip=0&take=1000 | After 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
| Scenario | Client behavior | Expected API result |
|---|---|---|
| Name missing | Browser required validation blocks submission. | No request is sent. |
| Assigned provider has no categories | Shows Select at least one category for every assigned provider. | No request is sent. |
| Delete cancelled | Table remains unchanged. | No request is sent. |
| API validation failure | resellerFormError 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.
backendIdis sent asnullwhenNo backendis selected.
Supplier management
Provider administration
Create suppliers, update service categories, credentials, status, and carrier display behavior.
| Name | Type | Categories | Carrier | Status | |
|---|---|---|---|---|---|
| Loading providers... | |||||
Trip static catalogue
Test Trip static data
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
| Action | Method | Endpoint | Trigger |
|---|---|---|---|
| List providers | GET | /api/v1/providers?skip=0&take=1000 | After login and after create/update/delete/status changes. |
| Create provider | POST | /api/v1/providers | Submitting the provider form in create mode. |
| Update provider | PUT | /api/v1/providers/{id} | Submitting the provider form in edit mode. |
| Delete provider | DELETE | /api/v1/providers/{id} | Delete button in the provider table after confirmation. |
| Change provider status | PATCH | /api/v1/providers/{id}/status | Activate/Deactivate button in the provider table. |
| Test country catalogue | GET | /api/v1/providers/{id}/static-data/countries?page=1 | Test Trip countries. Uses only the provider credentials JSON. |
| Test city catalogue | GET | /api/v1/providers/{id}/static-data/cities?page=1 | Test Trip cities. Uses only the provider credentials JSON and follows Trip pagination. |
| Refresh resellers | GET | /api/v1/resellers?skip=0&take=1000 | After provider changes so assignments and stats stay current. |
Create Request
| Field | Type | Notes |
|---|---|---|
type | number | 0 Trip, 5 MontyEsim, 10 Viator, 15 GooglePlaces, 16 ElifeRide, 17 TuneProtect, 20 Telegram. |
status | number | 0 Active, 1 Inactive. |
carrierDisplay | number | 0 Marketing, 1 Operating. |
categories | number | Bitmask: 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
| Scenario | Client behavior | Expected API result |
|---|---|---|
| Name or URL missing | Browser required validation blocks submission. | No request is sent. |
| No service category selected | Shows Select at least one service category. | No request is sent. |
| Required credential field left empty | Shows Fill in the required credentials: ... | No request is sent. |
| API validation failure | providerFormError 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:
0for activate and1for 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… | |||||||
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.
Step 1
How sure is sure enough?
Every comparison produces a score from 0 to 100. These two numbers decide what happens to it.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| 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.
| Name | Legacy role | Assigned roles | Assigned resellers | ||
|---|---|---|---|---|---|
| Loading users... | |||||
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
| Action | Method | Endpoint | Permission | Trigger |
|---|---|---|---|---|
| List roles | GET | /api/v1/roles | roles.view | After login and after create/update/delete. |
| Get role | GET | /api/v1/roles/{id} | roles.view | Opening a role in edit mode (loads its permission ids). |
| List permissions | GET | /api/v1/roles/permissions | roles.view | After login, to populate the permission checkboxes. |
| Create role | POST | /api/v1/roles | roles.create | Submitting the role form in create mode. |
| Update role | PUT | /api/v1/roles/{id} | roles.update | Submitting the role form in edit mode. |
| Delete role | DELETE | /api/v1/roles/{id} | roles.delete | Delete button in the role table after confirmation. |
| List users | GET | /api/v1/users | users.view | After login and after any create/delete/role change. |
| Create user | POST | /api/v1/users | users.create | Submitting the user form. |
| Delete user | DELETE | /api/v1/users/{id} | users.delete | Delete button in the user table after confirmation. A user cannot delete their own account. |
| Assign role | POST | /api/v1/users/{id}/roles/{roleId} | users.update | Checking a role checkbox in the "Manage access" panel. |
| Remove role | DELETE | /api/v1/users/{id}/roles/{roleId} | users.update | Unchecking a role checkbox in the "Manage access" panel. |
| Assign reseller | POST | /api/v1/users/{id}/resellers/{resellerId} | users.update | Checking a reseller checkbox in "Manage resellers" - rejected if the acting admin doesn't have access to that reseller themselves. |
| Remove reseller | DELETE | /api/v1/users/{id}/resellers/{resellerId} | users.update | Unchecking 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"]
}
]
}