Common Setup
Travelio — shared configuration for the web app and the mobile app
Developed By: BugBuild Labs
Common Setup
The configuration shared by the web application and the mobile app. Do these once, after
the install, and every part of Travelio behaves the way you expect. Almost everything is
entered in the admin panel under , not in
.env, so you can change it later without touching the server.
- .env reference — every key the application reads
- API host & application key and the developer API reference
- Mail, reCAPTCHA, social login
- Payment gateways — bank transfer, bKash, SSLCOMMERZ, JazzCash, Easypaisa
- SMS, push notifications and WhatsApp
- Live flight search (Amadeus, Sabre, Travelport, Duffel, TBO, Kiwi) and the AI trip planner
- File storage, languages and RTL
- Multi-vendor marketplace, loyalty, reviews and booking policy
- Queue and scheduler, backups and System Update, upgrading
- Go-live checklist
What Travelio is, and is not. It is a travel-agency ERP and booking system: you sell your own packages, hotels, transport, visa, Hajj & Umrah services (and, optionally, those of marketplace vendors) and the desk confirms them. Live supplier-API synchronisation is not bundled. The only live connections to outside inventory are the optional flight-search APIs (Amadeus, Sabre, Travelport, Duffel, TBO Air, Kiwi.com) and Amadeus hotel search described under Flights & AI; flight bookings remain request-to-book, confirmed by your desk. Services you do not sell can be switched off under Settings → Modules.
.env Reference
Copy .env.example to .env. The guided installer (/install) writes
the database, URL, timezone and key for you. After any change to .env run
php artisan optimize:clear (then php artisan optimize on production).
| Key | Default in .env.example | What it does |
|---|---|---|
APP_NAME | Travelio | Fallback application name (the agency name you set in Settings → General is what customers see). |
APP_ENV | local | Set production on a live site. |
APP_KEY | empty | Encryption key. Created by the installer or php artisan key:generate. Never change it on a live site: the mail password, Gmail OAuth secrets, Google/Facebook client secrets and the Amadeus, OpenAI and WhatsApp credentials are stored encrypted with it and would become unreadable. |
APP_DEBUG | false | Must stay false in production; true shows stack traces to visitors. |
APP_URL | http://localhost | The real public address, with https. Used to build the /storage image URLs, the payment callback URLs, the social-login redirect URIs and the web-origin exemption for the application key. |
APP_TIMEZONE | Asia/Dhaka | Application timezone for dates, schedules and the scheduler. |
APP_DEMO | false | Public-demo mode: blocks the destructive admin actions. Keep false on a real site. |
APP_INSTALLED | false | The installer sets it to true when it finishes. While it is false, the site sends visitors to /install. |
APP_API_KEY | not in the example | Optional bootstrap fallback for the mobile application key. The value saved in Settings → API Security always wins. |
FACEBOOK_GRAPH_VERSION | v23.0 | Graph API version used to verify Facebook sign-in tokens. |
HTTP_CA_BUNDLE | empty | Path to a CA bundle for outbound HTTPS (Google/Facebook sign-in verification). Set only if your PHP's cURL reports certificate errors. |
FRONTEND_URL, CORS_ALLOWED_ORIGINS | not in the example | Only for a separate web front end calling the API from another domain (comma-separated list for the second). The bundled website and the mobile apps do not need them. |
LOG_CHANNEL, LOG_LEVEL, LOG_DEPRECATIONS_CHANNEL | stack, debug, null | Logging. Use LOG_LEVEL=error in production. Payment diagnostics go to storage/logs/payments-*.log (kept 90 days), messaging to sms-*.log (14 days). |
DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD | mysql, 127.0.0.1, 3306, travelio, root, empty | Database connection. Use the full cPanel-prefixed names. |
BROADCAST_DRIVER, CACHE_DRIVER, SESSION_DRIVER, SESSION_LIFETIME | log, file, file, 120 | Standard Laravel drivers. Leave as is on shared hosting. |
QUEUE_CONNECTION | sync | Travelio does not push any work onto a queue (see Queue & Scheduler), so sync is correct. |
FILESYSTEM_DISK | local | Default disk. Uploads use the public and local disks (see File Storage). |
MAIL_MAILER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_ENCRYPTION, MAIL_FROM_ADDRESS, MAIL_FROM_NAME | smtp, smtp.example.com, 587, … | Bootstrap defaults only. Outgoing mail is configured in Settings → Mail, which overrides these at send time. |
MEMCACHED_HOST, REDIS_*, AWS_*, PUSHER_*, VITE_PUSHER_* | placeholders | Standard Laravel skeleton placeholders. Travelio does not need them; leave blank unless you deliberately move cache, sessions or a disk to Redis/S3. |
DEMO_*_EMAIL, SANCTUM_TOKEN_PREFIX, ASSET_URL | not in the example | Optional: addresses behind the demo login buttons, a prefix for API tokens, and a CDN base URL for assets. |
Not in .env on purpose: mail, payment gateways, social login, SMS, push, WhatsApp, Amadeus, OpenAI, API application key, loyalty and booking policy are all saved in the database from the admin panel (the mail, Gmail, social, Amadeus, OpenAI and WhatsApp secrets are stored encrypted; keep the database private either way), so going live needs no redeploy.
API & Mobile App Connection
The mobile app reads and writes through the Laravel REST API at
https://yourdomain.com/api/v1. Nothing extra to install — the API ships with the
backend and is live as soon as the site is.
Point the app at your server with two values, kept in a small file under the app's env/
folder (full detail in Travelio App):
// env/prod.json
{
"API_HOST": "https://yourdomain.com",
"APP_API_KEY": "travelio-app-key-7c2e9a4d1f8b6035"
}
flutter run --dart-define-from-file=env/prod.json
Do not put /api/v1 in API_HOST; the app appends it.
| Environment | API_HOST |
|---|---|
| Live server | https://yourdomain.com |
| Physical Android phone on your LAN | http://<your-computer-ip>:8000 |
| Android emulator | http://10.0.2.2:8000 |
| iOS simulator / desktop | http://127.0.0.1:8000 |
Check it quickly in a browser — the API should answer, not 404:
https://yourdomain.com/api/v1/home
For local testing over the LAN, run the backend as php artisan serve --host=0.0.0.0, otherwise it only listens on localhost and the phone cannot reach it.
Application key (Settings → API Security)
Every API request from outside a web browser must carry the shared application key in an
X-App-Key header. A fresh install seeds the key
travelio-app-key-7c2e9a4d1f8b6035, the same value compiled into the stock app, so a
new backend and an unmodified app work together with nothing to configure.
- shows the key. Generate creates a new random one; Copy copies it; clearing the field and saving switches the check off.
- After you change the key you must rebuild and republish the app with the new
APP_API_KEY. Until then every request, including installed copies on customers' phones, fails with401 Invalid or missing application key. - Requests made by the website itself from its own domain (
APP_URL,FRONTEND_URLorCORS_ALLOWED_ORIGINS) are exempt, because a browser cannot hold a secret.
The key ships inside the app, so it can be extracted. It is a filter against casual scraping and a rotation kill-switch, not authentication. Real protection is the Sanctum token on private routes and the rate limits below.
Authentication, format and limits
- Tokens: Laravel Sanctum bearer tokens. Log in, then send
Authorization: Bearer <token>. Tokens do not expire on their own;POST /auth/logoutrevokes the current one. - Envelope: every response is
{ "success": true|false, "message": "…", "data": … }. Validation errors are HTTP 422 with the field errors indata. - Rate limits: 60 requests/minute per user (or IP) overall; login, register and social sign-in 5/minute per IP and per account; OTP verify and password reset 5/minute per IP plus 10 per 10 minutes per account; quote 30/minute; live flight/hotel search 20/minute; live hotel booking 10/minute; AI trip plans 10/minute.
- Marketplace: the
multi_vendor_enabledflag inGET /settingstells the app whether to show vendor screens.
Developer API Reference
Base URL https://yourdomain.com/api/v1. Send Accept: application/json and
X-App-Key on every call, plus Authorization: Bearer … where the table says
Token. Public routes need only the application key.
Customer-only routes answer 403 to an agent or staff token.
Sign-in
| Method & path | Access | Notes |
|---|---|---|
POST /auth/register | Public | name, phone, email, password, password_confirmation, gender, optional date_of_birth. Returns requires_otp: true; the 6-digit code is emailed, so mail must work. |
POST /auth/login | Public | login (email or phone) + password. Customers, agents, staff and guides all use it. |
POST /auth/otp/request, /auth/otp/verify | Public | email or phone; verify also takes otp and returns the token. |
POST /auth/reset-password | Public | email|phone, otp, password, password_confirmation. |
POST /auth/social/google, /auth/social/facebook | Public | Body id_token (Google) or the Facebook access token; verified server-side, then the normal token is issued. GET /auth/social/providers returns which are enabled. See Social Login. |
GET /auth/me, POST /auth/logout | Token | Current account; revoke token. |
POST /api/v1/auth/login
{ "login": "[email protected]", "password": "12345678" }
200 { "success": true, "message": "Login successful.",
"data": { "token": "12|abc…",
"account": { "id": 1, "account_type": "customer", "name": "…",
"email": "…", "phone": "…", "role": null, "profile_photo": null } } }
401 { "success": false, "message": "Invalid credentials.", "data": [] }
account_type is customer, agent, staff or guide; the app uses it to route to the right screens.
Public catalogue and content
| Method & path | Notes |
|---|---|
GET /home, /categories, /settings | /home: sliders, promotions (each with target {type, id, url}; type = package | hajj_package | visa_service | hotel | url), categories, featured_tours (featured first), latest_tours, featured_hajj_packages and modules. /settings: app branding, currency symbol, default language, cancellation policy, multi_vendor_enabled, modules (module key → true/false, from Settings → Modules), agent_registration_enabled and vendor_registration_enabled. |
GET /tours, /tours/{id} | Filters search, category_id, min_price, max_price, vendor_id, featured=1; sort = price_low | price_high | latest (default: featured first, then newest); each tour has is_featured; per_page up to 50; pagination in meta. |
GET /hotels, /hotels/{id}, /live-hotels | vendor_id filter. live-hotels (city_code, check_in, check_out, adults, room_quantity) needs Amadeus. |
GET /hajj/packages, /hajj/packages/{id} | type (Hajj|Umrah), search, vendor_id, featured=1; featured first, then cheapest; each has is_featured. |
GET /transport/types, /transport/vehicle-categories, /transport-services, /flight-routes | transport-services accepts vendor_id. |
GET /live-flights | origin, destination (IATA), departure_date, return_date, adults, currency. Searches every enabled flight provider; returns flights (normalised offers: provider, provider_label, id, price {total, currency}, validating_airline, itineraries [{direction, duration, stops, segments}], segments [{from, to, departure_at, arrival_at, carrier, flight_number, duration}], stops, duration (minutes), seats_left, refundable) and providers (which answered). Answers 503 with a message when none is configured or all fail. |
GET /visa-services, /visa/track | Visa catalogue (vendor_id filter) and reference tracking. |
GET /packages/{id}/reviews | Approved reviews (paginated) with avg_rating and reviews_count. |
POST /become-a-vendor | Vendor sign-up from the app (creates the login and a pending vendor). Throttled; 404 while multi-vendor is off. |
/vendor/* (signed in as a vendor) | dashboard, profile (POST with an optional logo), listing-kinds, listings/{kind} (POST create / POST {id} edit with an optional image, DELETE), bookings, bookings/{type}/{id}/{confirm|decline}, reviews, reviews/{id}/reply, earnings, statement, payouts. Money figures: available (negative = owed to the agency), held, adjustments, owed, min_payout, tds_rate, hold_days. |
GET /vendors, /vendors/{idOrSlug} | Marketplace storefronts (search, type = Tours|Hotels|Transport|Hajj & Umrah|Visa). 404 while multi-vendor is off. The detail response carries ready-made listing URLs filtered by vendor_id. |
GET /faqs, /content-blocks, /testimonials, /enquiry-types, /contact-info | CMS content. |
POST /become-an-agent, /enquiries, /contact | Public forms; each creates a CRM lead or message for the desk. become-an-agent answers 403 while agent registration is closed. |
Switched-off modules: every endpoint of a module switched off under Settings → Modules answers 403 {"success": false, "message": "This service is not available.", "data": {"module": "hotels"}}. Agent accounts get 403 at login while the Agents module is off. | |
GET /app-languages, /app-language/terms | Languages and phrase list from the admin Language module (?language_code=bn selects one; unknown codes fall back to English). |
Customer — bookings, quote and payment
| Method & path | Notes |
|---|---|
POST /bookings/quote | package_id, travelers, optional coupon_code, points_redeemed. Prices without saving: gross, discounts, payable amount, points balance and limits. |
GET /bookings (?status=), POST /bookings, GET /bookings/{id} | Create: package_id, travel_date, travelers, optional customer_name/phone/email, needs_visa, notes, coupon_code, points_redeemed. An unusable coupon never fails the booking; the reason comes back in discount_notes. |
POST /bookings/{id}/pay | See the payment example below. |
GET /payments/bank-accounts | Active company bank accounts to show for a transfer: bank name, account title/number, IBAN, branch, SWIFT, currency, instructions. |
POST /bookings/{id}/cancel, /bookings/{id}/guide-rating | Cancel obeys the booking policy; guide rating after the trip. |
GET /reviews, POST /bookings/{id}/review | Review body: rating (1–5), optional title, comment. Only for a tour the customer paid for and has travelled on; one per trip. |
POST /hotel-bookings, GET /hotel-bookings, PUT /hotel-bookings/{id}, POST …/{id}/pay, …/{id}/cancel | hotel_id, hotel_room_id, check_in, check_out, optional guest details. |
POST /hotel-bookings/live | Books an Amadeus offer (offer_id, guest names, email, phone). Needs Amadeus. |
GET /flights, POST /flights, POST /flights/{id}/cancel | Request-to-book: route (e.g. DAC-DXB), flight_date, optional return leg, passenger and airline. The desk prices and tickets it. |
GET /transport, POST /transport, GET /transport/{id}, POST …/{id}/pay, …/{id}/cancel, GET /transport/drivers | Request-to-book transport trips. |
GET /visa, POST /visa, GET /visa/{id}, POST /visa/{id}/pay, GET /visa/documents/{id}/download | Visa applications and their documents (private files, owner only). |
GET /hajj/registrations, POST /hajj/register | Hajj & Umrah registration. |
Hotel, transport and visa …/pay endpoints take the same method and bank-proof fields as the booking one.
Accepted methods: Cash, Bank, Card, bKash, Nagad, JazzCash, Easypaisa, Wallet.
POST /api/v1/bookings/42/pay (online gateway enabled, e.g. bKash)
{ "method": "bKash" }
200 { "success": true, "message": "Complete the payment to confirm this booking.",
"data": { "online": true, "gateway": "bkash", "label": "bKash",
"reference": "PAY-…", "amount": 25000,
"redirect_url": "https://…/checkout",
"callback_url": "https://yourdomain.com/payment/callback/bkash/PAY-…" } }
The app opens redirect_url in a web view and watches for callback_url; the result page then
hands the payer back with the deep link travelio://payment-result?status=paid&reference=…. Then re-read the booking:
payment_status is the truth, not the redirect.
POST /api/v1/bookings/42/pay (multipart/form-data — manual bank transfer)
method=Bank
bank_account_id=3
reference=TRX-77821 (or upload a receipt; one of the two is required)
paid_on=2026-10-02 (not in the future)
[email protected] (jpg, jpeg, png, webp or pdf, max 4 MB)
200 { "success": true, "message": "Payment submitted — we will confirm it shortly…",
"data": { "booking": { …, "payment_status": "pending_verification" },
"awaiting_confirmation": true, "wallet_balance": 0 } }
409 "Your payment is already awaiting verification." | "Booking is already paid."
A booking carries payment_status (unpaid, pending_verification, paid, rejected) and
supplier_status (not_required, pending, confirmed, failed) alongside its own status.
Cash, card or wallet with no gateway enabled is a declared payment the desk confirms; it is never marked paid on the customer's word.
Customer — account, billing and extras
| Method & path | Notes |
|---|---|
GET /dashboard | Summary mirroring the portal dashboard. |
GET /wallet | Balance and transactions. There is deliberately no top-up endpoint: a wallet is funded only by a real receipt/refund or a staff-recorded cash adjustment. |
GET /loyalty | Points, referral code, transactions and the redemption rules (rate, value, min, max_percent, can_redeem_now). |
GET /trip-plans, POST /trip-plans | AI itinerary: destination, start_date, days (1–30), optional budget, interests. Needs OpenAI; 503 with a message otherwise. |
GET /invoices, /invoices/{id}, /payments; GET /invoices/{id}/pdf, /receipts/{id}/pdf, /hotel-bookings/{id}/voucher, /flights/{id}/eticket | Billing history and printable PDFs, scoped to the signed-in customer. |
GET /documents | Documents the agency holds for the customer (read-only). |
GET/POST /passports, PUT/DELETE /passports/{id}; GET/POST /travelers, PUT/DELETE /travelers/{id} | Saved passports and travellers. |
GET/PUT /profile, POST /profile/avatar, POST /profile/password, GET/PUT /preferences | Profile, photo, password and notification/language preferences (customers and agents/staff). |
GET /notifications, POST /notifications/{id}/read, POST /notifications/read-all | In-app notification list. |
POST /device-tokens, DELETE /device-tokens | Register / forget this phone for push: token (max 512) and optional platform = android|ios. Call the DELETE on logout. |
GET/POST /support/tickets | Support tickets. |
POST /api/v1/bookings/quote
{ "package_id": 7, "travelers": 2, "coupon_code": "WELCOME10", "points_redeemed": 200 }
200 { "success": true, "message": "Quote ready.",
"data": { "quote": { "gross_amount": 50000, "coupon_discount": 5000,
"points_redeemed": 200, "points_discount": 200,
"total_discount": 5200, "amount": 44800,
"coupon_error": null, "points_error": null,
"points_balance": 900, "points_max": 4500, "points_min": 100,
"points_rate": 1, "points_max_percent": 20 } } }
Guide and agent
| Method & path | Notes |
|---|---|
GET /guide/assignments, POST /guide/assignments/{id}/start, …/complete | Tour-guide role. |
GET /agent/dashboard, /agent/wallet, /agent/commissions, /agent/customers, /agent/invoices, /agent/transactions, /agent/reports, /agent/support | B2B agent screens. |
POST /agent/wallet/withdraw | Withdrawal request. |
GET/POST /agent/bookings, GET/PUT /agent/bookings/{id}, POST /agent/bookings/{id}/cancel | Tour bookings sold to clients; amend or drop while the desk has not priced it. |
POST /agent/hotel-bookings, /agent/transport-bookings, /agent/flight-bookings | Hotel, transport and flight requests for clients. |
Not in the API: the wishlist heart and the vendor portal (listings, earnings, payouts) are web-only features; vendors sign up and work in the browser at /become-a-vendor and /portal/vendor. The API only reads the marketplace (/vendors and the vendor_id filters).
Mail drives sign-up verification codes (the code is emailed, not texted), password resets and booking confirmations on the website, the portals and the app. A new customer cannot finish registering until mail works.
Configure it in ; the saved values override the
MAIL_* keys in .env. Three drivers are offered:
| Driver | Fields | Use it when |
|---|---|---|
| SMTP | Host, Port, Username, Password, Encryption (TLS/SSL), From address, From name | Normal case. Port 587 with TLS, or 465 with SSL. |
| Sendmail | Sendmail path, From address, From name | The server has a local mail transfer agent. |
| Gmail API | Client ID, Client Secret, Refresh Token | Your host blocks outbound ports 587 and 465. Sends over HTTPS using a Google OAuth client you create in Google Cloud Console (Gmail API enabled) and a refresh token for the sending mailbox. |
Gmail over SMTP needs an App Password, not your account password. Use the Send test email box on the same page, and confirm the message actually arrives, before you announce the site.
Mail is sent immediately while the request runs; there is no mail queue to start (see Queue & Scheduler).
reCAPTCHA
stores a Google reCAPTCHA site key and secret key and an Active/Inactive switch (create the keys at google.com/recaptcha/admin, adding your domain).
Important: in this release the keys are only stored — none of the bundled public forms (login, sign-up, contact, booking enquiry, vendor sign-up) renders the reCAPTCHA widget or verifies the response. Do not rely on it for bot protection. The public forms are instead protected by rate limiting (for example vendor sign-up 10/minute, API login 5/minute, OTP 5/minute). The settings are there so a developer can wire the widget into a form of their choice.
Payment Gateways (online booking payments)
Customers can pay for tour, hotel, transport and visa bookings from the mobile app and the customer portal. Five ways to take payment are built in. The four online gateways each call the provider's own REST/hosted-checkout API (no SDK or plugin); manual bank transfer needs no provider at all.
| Method | Credentials (Settings → Payment Gateways) | Currency |
|---|---|---|
| Bank transfer (manual, admin-verified) | Company bank accounts, no provider keys | Any |
| bKash (Tokenized Checkout) | App Key, App Secret, Username, Password | BDT |
| SSLCOMMERZ (hosted checkout — cards, mobile banking, net banking) | Store ID, Store Password | BDT |
| JazzCash (Pakistan, hosted checkout v1.1) | Merchant ID, Password, Integrity Salt | PKR |
| Easypaisa (Pakistan, Easypay) | Store ID, Hash Key, Account Number, Inquiry API Username, Inquiry API Password | PKR |
Currency: the amount on the booking is sent to the gateway as is — there is no conversion. bKash and SSLCOMMERZ charge in BDT, JazzCash and Easypaisa in PKR (JazzCash receives paisa, PKR × 100). Enable only the gateway that matches the currency you price in.
How an online gateway is switched on
Credentials are entered in , not in
.env, so going live needs no redeploy. Each gateway has a Sandbox switch (on by default) and an
Active switch: a gateway is offered to customers only when every credential is filled in and it is
switched on (a placeholder value of xxx counts as empty). The same page lists the callback URL pattern.
Every payment goes through a payment_intents record that lives for 60 minutes; a stale callback cannot settle a booking later.
The provider's browser return and its server notification both land on:
https://yourdomain.com/payment/callback/{gateway}/{reference}
{gateway} = bkash | sslcommerz | jazzcash | easypaisa (GET or POST, no login, no CSRF token)
The reference is generated per payment and passed to the provider automatically, so there is normally nothing to paste into the provider's
dashboard — only make sure your domain is the one registered with the merchant account and is reachable over https.
Nothing in the callback is trusted: the gateway re-validates with the provider (and checks amount, currency and reference against the intent) before the booking's
payment_status becomes paid and the invoice and receipt are raised. A repeated callback is a no-op. The reason for any rejection is written to
storage/logs/payments-YYYY-MM-DD.log.
| Gateway | How it is verified | Sandbox / live |
|---|---|---|
| bKash | On return, grants a token and calls Execute; only Completed with a transaction id settles. | Sandbox tokenized.sandbox.bka.sh, live tokenized.pay.bka.sh. Use the sandbox credentials and test wallet from bKash. |
| SSLCOMMERZ | Calls the validation API with the returned val_id; transaction id, amount (not short) and currency must match. | Sandbox sandbox.sslcommerz.com, live securepay.sslcommerz.com. Sandbox store credentials come from SSLCOMMERZ. |
| JazzCash | The result is believed only if its pp_SecureHash (HMAC-SHA256 with your Integrity Salt) matches; response code 000, reference and amount must match. | Sandbox sandbox.jazzcash.com.pk, live payments.jazzcash.com.pk. Use your JazzCash merchant sandbox. |
| Easypaisa | Two-leg Easypay checkout; the final return is checked with the Inquire Transaction API (must be PAID, amount not short). | Sandbox easypaystg.easypaisa.com.pk, live easypay.easypaisa.com.pk. Use your Easypaisa merchant sandbox. |
Test each gateway against the provider's sandbox first, then make one small live transaction before announcing online payment. Merchant onboarding and fees are between you and the provider. JazzCash and Easypaisa were integrated to the providers' documented APIs and need your own merchant sandbox to confirm end to end.
Manual bank transfer with admin verification
- Add your accounts in (bank, account title and number, IBAN, branch, SWIFT, currency, instructions, active flag). Active accounts are shown to a customer who chooses Bank, in the portal and through
GET /api/v1/payments/bank-accounts. - The customer sends the transaction reference, payment date and a receipt (JPG, PNG, WebP or PDF, up to 4 MB) from the portal or the app. The booking shows Pending verification; nothing is paid on their word.
- The desk opens , views the proof, records bank account, reference, amount received and date, then approves or rejects. Approval settles the invoice, issues the receipt with the bank reference and, for an agent sale, creates the commission. Rejection tells the customer why and lets them resubmit. An amount below what is due cannot be approved.
Works for package, hotel, transport and visa bookings, over the web and the API. Receipts are kept on the private disk and served only to staff. Needs the existing Booking and Accounting permissions; no role changes.
With no online gateway enabled, bookings are still recorded and paid at the desk (cash, bank, card, mobile banking) or settled from the customer's wallet — exactly as before.
SMS, Push Notifications & WhatsApp
All three are optional, are configured in the admin panel, and never block a booking or payment if they fail.
| Channel | Where | What you enter | Used for |
|---|---|---|---|
| Push (FCM) | Settings → Push Notifications | The Firebase project's service-account JSON (Firebase console → Project settings → Service accounts → Generate new private key; paste the whole file) and an Active switch | Every in-app notification is mirrored to the account's registered devices over FCM HTTP v1. |
| WhatsApp Cloud API | Settings → WhatsApp | Access Token, Phone Number ID, API version (default v21.0) | A text message to the customer's phone when a booking's status changes. |
| SMS (MiM SMS) | Settings → SMS | Username, API key, Sender ID, Active switch | Gateway with balance display and a Send test SMS form. Sign-up and password-reset codes go by email, not SMS, so SMS is not required. |
Push needs the app side too: add your own google-services.json / GoogleService-Info.plist to the Flutter app (see Travelio App); the app registers its token with POST /device-tokens after login.
The WhatsApp message is a free-form text sent from your Cloud API number, so Meta's own messaging-window rules apply; a rejected message is logged and the booking carries on.
Push failures are logged to storage/logs/sms-*.log and WhatsApp failures to the Laravel log, rather than shown to the customer.
Flights & AI (flight APIs and OpenAI)
Optional integrations. Leave them blank and everything else works.
| Integration | Where | Fields (defaults) | Enables |
|---|---|---|---|
| Amadeus Self-Service | Settings → Flight API | API key, API secret, base URL (https://test.api.amadeus.com; use https://api.amadeus.com for production keys) | Live flight-offer search and live hotel offer search and booking (/live-hotels, /hotel-bookings/live). |
| Sabre (Bargain Finder Max) | Settings → Flight API | Client ID, client secret, PCC, base URL (https://api.cert.platform.sabre.com; production https://api.platform.sabre.com) | Live flight-offer search. |
| Travelport (JSON Air API / Travelport+) | Settings → Flight API | Username, password, client ID, client secret, access group, OAuth URL (https://oauth.pp.travelport.com/oauthahs/oauth2/token), API URL (https://api.pp.travelport.com/11/air) — pre-production by default | Live flight-offer search. |
| Duffel | Settings → Flight API | Access token (a duffel_test_… token uses test mode), base URL (https://api.duffel.com) | Live flight-offer search. Prices come in your Duffel account's currency. |
| TBO Air (Tek Travels) | Settings → Flight API | Client ID, username, password, authenticate URL and search URL (TBO's test hosts by default) | Live flight-offer search. |
| Kiwi.com (Tequila) | Settings → Flight API | API key, base URL (https://api.tequila.kiwi.com) | Live flight-offer search. Tequila no longer accepts new partners — only for agencies that already have a key. |
| OpenAI | Settings → AI Settings | API key, model (gpt-4o-mini), base URL (https://api.openai.com/v1; any OpenAI-compatible endpoint works) | The customer's AI trip planner (/trip-plans). |
Scope. This is a search-and-request connection, not a supplier-API synchronisation. Flight offers are for the customer to choose from; the flight booking itself is a request your desk prices and tickets. Test environments return limited sample data; get production access from each provider before relying on it. API usage is billed to you by the providers and OpenAI.
How several flight providers work together
- Tick Enabled on each provider you have credentials for and save. A provider shows Configured once its required fields are filled in. Secrets are stored encrypted; leaving a secret field blank keeps the saved one.
- A search goes to every enabled, configured provider at the same time. Offers are merged, duplicates (same carrier, flight numbers and departure times) are collapsed to the cheapest, and the list is sorted by price, then duration. Each offer names its provider.
- One provider failing (wrong key, timeout, outage) is written to
storage/logsand does not stop the others. Only when none is configured, or every one fails, does the search fall back to the request-a-fare form, with a generic message. - An install upgraded from 1.4.0 keeps searching Amadeus with its saved keys until you enable more providers.
File Storage
Two disks are used:
- Public — package photos, blog images, logos, vendor logos. Written straight into
public/uploads(public_html/uploadson a cPanel Option B install), so no symlink is needed. - Private (
local) — visa documents and bank-transfer receipts. Written tostorage/app(visa-documents/…,payment-proofs/…), with no public URL; the app streams them only to the owner or to authorised staff.
public/uploads, storage/ and bootstrap/cache/ must be writable by PHP.
php artisan storage:link is optional: Travelio itself does not serve files from storage/app/public.
Broken images almost always mean APP_URL in .env does not match the real domain (including https), or public/uploads is not writable.
Upgraded from 1.0.x? Visa documents used to sit on the public disk. Finalize moves them to private storage automatically; you can also run php artisan travelio:move-private-documents (add --dry-run first to preview).
Languages & RTL
Travelio ships with English, Bangla, Arabic (right-to-left) and French active. Every phrase is editable from in the admin sidebar — you do not need to touch the translation files.
- Add a language: create it there (name, code, flag icon, text direction
LTRorRTL), then translate the phrase list. - Direction: choose
RTLfor Arabic, Urdu, Hebrew and similar; the layout mirrors automatically. - Mobile app: it reads the same languages and phrases from
GET /app-languagesand/app-language/terms, and the default fromGET /settings, so one edit serves the web and the app.
Multi-Vendor Marketplace
Lets outside hotels, tour operators, transport providers, Hajj & Umrah operators and visa agents list and sell through your site. It is on for a fresh install and stays off after an upgrade from an earlier version until you switch it on (single-agency mode). You can switch it off at any time.
Settings
In . Every money rule below is optional and defaults to off / zero, so nothing changes until you set it.
| Setting | Meaning |
|---|---|
| Multi-vendor marketplace | Off — single agency / On — vendors can sell. Off hides vendor sign-up, the vendor portal, public vendor pages and the API's /vendors (404); the app learns the state from multi_vendor_enabled in /settings. Nothing is deleted when you switch it off. |
| Default commission (%) | 0–100, default 10. The share the agency keeps from each vendor sale. |
| Commission % per service | Optional rates for Tours, Hotels, Transport, Hajj & Umrah and Visa. Blank = the default above. |
| Minimum commission per sale | The agency keeps at least this much from every sale (never more than the sale itself). 0 = no minimum. |
| VAT on the agency commission | Off / On — charged to the vendor, with a VAT rate (%). When on, VAT on your commission is deducted from the vendor's share and moved from Sales Revenue to VAT Payable. A vendor can be marked VAT exempt on their page. |
| Holding period (days) | Days a vendor's share is held before it can be withdrawn (0 = at once). Count the holding period from the payment date, or from the service date (trip/stay) — a trip already taken is held from the payment date. |
| Tax withheld from payouts (TDS %) | Deducted from each payout when it is approved (fixed at that moment) and booked to Vendor TDS Payable. The vendor receives the amount less this tax. |
| Minimum payout | Vendors cannot request less than this. |
| Automatic payout requests | Off / Weekly (Mondays) / Monthly (1st). Raises a request (via the daily vendor:auto-payouts command, 03:00) for each approved vendor who has saved payout details and a withdrawable balance above the minimum. Needs the same cron entry as the rest of the scheduler. |
| Approve automatic payouts without the office | Off by default. On = automatic requests are approved at once. |
| Mark automatic payouts as paid | Off by default. On = they are also booked as paid at once. Only for agencies whose bank or wallet pays vendors on a standing schedule — the system books the payment; it does not transfer money. |
Per vendor (): status, a commission % or a flat fee per sale, VAT exempt, a note to the vendor. Commission precedence: flat fee → the vendor's own % → the service-type % → the default %, then the minimum commission is applied. The effective rate and amount are frozen on each sale when it is first credited, so changing a rate never rewrites history.
How it works
- A vendor registers at
/become-a-vendor(or from the app). They are Pending and can sign in, but cannot list or withdraw. The desk is notified. - Staff review them in : Approved, Suspended or Rejected, with commission terms and a note. The vendor is notified of each change.
- An approved vendor adds listings in the portal (
/portal/vendor) or the app, with photos: tours, hotels, transport services, Hajj & Umrah packages, visa services. Each new or edited listing is pending until staff approve it; only approved listings go live on the site and in the app. - When a customer's booking of a vendor's service is paid, an earning is created: gross, commission (and VAT), vendor net. Visa: only your service fee counts (the government fee is passed to the embassy). Hajj: once the pilgrim is settled in full. Transport: the office assigns the vendor to the trip. Booked as Vendor Cost of Sales / Vendor Payable; reversed if the booking stops being paid.
- Refunds shrink the vendor's share in proportion to their part of the sale (a visa's service fee out of government + service fee); a full refund reverses it and undoing a refund restores it.
- The vendor requests a payout (Bank, Cash, bKash or Nagad) — or an automatic one is raised. The balance is re-checked when you approve and when you pay, so a refund after the request cannot overdraw the vendor. In you approve, download approved payouts as a bank file (CSV), make the transfer, then Mark all approved as paid (or mark them one by one). Paying posts the cash movement.
Balances, adjustments and negative balances
- Withdrawable now = earnings past their holding period + adjustments − payouts taken or requested. Held money is still inside the holding period.
- Balance adjustments (vendor page): a debit (penalty, clawback) or credit (bonus, correction) with a reason. Each is booked to the accounts, notifies the vendor and can be removed.
- If a refund or penalty arrives after a vendor was paid, their withdrawable balance goes negative: they cannot request a payout, and the debt is netted against their next sales automatically.
Reports and statements
- — per vendor for a date range: sales, gross, refunded, commission (your income), VAT on commission, vendor net, paid out, tax withheld, held, balance owed and withdrawable; downloadable as CSV.
- Each vendor has a Statement (portal → Earnings): a period's sales, adjustments, payouts and tax, printable or CSV; the same data is in the API at
GET /vendor/statement. - Accounting entries use the accounts Vendor Payable (2400), Vendor TDS Payable (2410), Vendor Cost of Sales (5500) and the existing VAT Payable;
php artisan accounting:rebuildre-posts them.
Notifications
In-app (and push, when Firebase is configured): vendors hear about approval/suspension, listing decisions, new paid sales, payouts and adjustments; the desk hears about new applications, listings awaiting approval, payout requests and declined bookings. Vendors can reply to reviews of their tours, which the customer is told about, and confirm or decline their bookings.
Mobile app (vendor role)
A vendor account signs in to the same app and lands on the vendor home: Dashboard, Listings (with photos), Bookings, Earnings and More (Business profile with logo and payout account, Reviews, Settings). Vendor screens are available in English, Bangla and Arabic (other languages show English); the phrases are served with the app texts as vendor_phrases. The vendor API lives under /api/v1/vendor/* (dashboard, profile, listing-kinds, listings, bookings, reviews, earnings, statement, payouts) and sign-up is POST /become-a-vendor; all of it is 403/404 while the marketplace is off.
Demo accounts
The demo seeder adds [email protected], [email protected] and [email protected] (password 12345678; the last is Pending). Running that seeder also switches the marketplace on (Settings → General → Marketplace turns it off again). On a demo install (APP_DEMO=true) the sign-in page then shows a Vendor quick-login button next to Customer; it is hidden while the marketplace is off or when the demo vendor account does not exist, and is never shown on the admin sign-in.
Where vendors find the links
Vendors use the normal sign-in page (/login) — there is no separate vendor login — and land on /portal/vendor/dashboard. While the marketplace is on, the public site shows Become a Vendor in the footer Company column and in the header Company dropdown (with Our Vendors), and the sign-in page shows a Become a vendor link. All of these hide themselves when the marketplace is off instead of leading to a 404. Existing sites get the menu items from a migration; to add or remove them yourself use .
The vendor portal lists (listings, bookings, payouts) look and work like the admin lists: the primary Add button sits top-right, tables have search, filter, sorting and paging, and a vendor's logo is used as their avatar in the header.
Roles: a Vendor role is added, and Admin/Super Admin gain the vendor_* permissions. Upgrading adds them for existing staff automatically (see Upgrading).
The marketplace is a catalogue-and-settlement layer for vendors you onboard yourself. It does not pull inventory from outside supplier APIs.
Loyalty, Reviews & Booking Policy
Loyalty & referrals — Settings → Loyalty & Referrals
| Setting | Default | Meaning |
|---|---|---|
| Points per currency unit | 0.01 | Points earned on what the customer actually paid (0.01 = 1 point per 100 of currency). A booking that stops being paid has its reward reversed. |
| Referral bonus points | 100 | Given to the referrer after a referred customer's first paid booking. |
| Value of one point | 1 | Currency discount per point when spent. 0 switches spending off. |
| Minimum points per redemption | 100 | Fewest points that may be spent at once. |
| Maximum share of a booking | 50 (%) | Largest part of a booking that points may cover, so some real money stays in every sale. |
| Publish reviews automatically | 0 (off) | Off (default): new reviews wait in for approval. A review is only possible for a tour the customer paid for and has travelled on. |
Points are a discount on the booking, never wallet credit, so invoices, receipts and the books stay in agreement. Coupons are managed in the Coupons module and apply on the app, the portal, the website and the back-office form.
Booking policy — Settings → General Settings → Booking Policy
| Setting | Default | Meaning |
|---|---|---|
| Cancellation window (hours) | 24 | A customer can cancel online only if the travel date is at least this far away. |
| Cancellation penalty (%) | 10 | Share of the paid amount the agency keeps when a customer cancels. Back-office cancellation is unaffected and stays a manual call. |
The rule is shown to customers before they cancel (and exposed in GET /settings). Review it after every upgrade from 1.0.x.
Queue Worker & Scheduler (cron)
No queue worker is required. Travelio sends mail, push, WhatsApp and payment calls while the request runs; none of it is put on a queue, and QUEUE_CONNECTION=sync is correct.
If you later add your own queued jobs, run php artisan queue:work --tries=3 --timeout=90 under Supervisor and switch the connection to database.
The scheduler is recommended. One cron entry runs everything Travelio schedules:
* * * * * cd /home/youruser/yourapp && php artisan schedule:run >> /dev/null 2>&1
On cPanel add it under (no terminal needed) — see Install on cPanel → Step 7.
| Task | When | What |
|---|---|---|
passports:refresh-statuses | daily 02:10 | Moves stored passport statuses (valid / expiring / expired) as dates pass. |
vendor:auto-payouts | daily 03:00 | Raises automatic vendor payout requests when Automatic payout requests is on (Marketplace settings). |
currencies:refresh-rates | daily 04:20 | Refreshes exchange rates when Settings → Currencies → Update rates automatically is on; does nothing otherwise. |
webhooks:deliver | every minute | Sends pending and retrying webhook deliveries (Settings → Webhooks). With a queue worker they go out at once instead. |
Other commands you may run by hand: php artisan accounting:rebuild (re-derive the ledger from invoices/receipts after migrating an existing database) and php artisan travelio:move-private-documents.
Backups & System Update
There is no in-app database-backup screen and no scheduled backup. Back up the database yourself — cPanel → Backup, phpMyAdmin Export, your host's snapshot, or mysqldump in a cron job — and keep copies off the server. A backup that only exists on the machine it protects is not a backup. Also keep .env, storage/app (private documents and receipts) and public/uploads.
installs a newer release:
- Take your own database and file backup first.
- Upload the release ZIP (the contents of
WebSourceCode, or the folder itself, containingtravelio-update.json). Onlyapp, bootstrap, config, database, lang, Modules, public, resources, routesare replaced (translation files are merged: new keys are added, your edited values are kept);.env,storage,vendor,public/uploadsandpublic/storagenever are, and a version that is not newer than the installed one is refused. - Travelio copies the files it is about to replace to
storage/app/updates/backups/(a file backup, not the database), puts the site in maintenance mode, replaces the files, runs migrations, refreshes permissions and roles (new permissions go to existing roles and to the users who hold them), adds new settings, languages and home sections without overwriting yours, records the new version and clears caches. Before replacing anything it checks the PHP version, extensions and required packages and stops with a clear message if something is missing.
cPanel fallback if ZIP upload or extraction is blocked: copy the contents of WebSourceCode over the project, choose Replace all, refresh the System Update page and press Finalize manual update. The ZIP route needs the PHP zip extension. If you changed vendor requirements, run composer install --no-dev --optimize-autoloader over SSH as well.
Upgrading
You can go straight from 1.0.0, 1.1.0, 1.2.0, 1.3.0, 1.4.0, 1.5.0 or 1.6.0 to 1.7.0 — migrations are cumulative and Finalize also adds the settings, permissions, language, home sections and private-document move that a plain migrate would miss. Tested on a real 1.0.0 install with demo data, and 1.5.0 → 1.6.0 with the ZIP upload on a 1.5.0 install with demo data. Back up files and database first. Because 1.0.0 and 1.1.0 have no System Update screen, the first jump is a file copy:
- Copy the contents of
WebSourceCodeover the project with Replace all, but not.env,vendor,storage,public/uploads,public/storage). With rsync use anchored excludes:rsync -avz --exclude='/.env' --exclude='/storage' --exclude='/vendor' --exclude='/public/uploads' --exclude='/public/storage' WebSourceCode/ /path/to/project/ - Sign in as super-admin, open Settings → System Update (every other admin page redirects there until the update is finalized) and press Finalize manual update — or run
php artisan travelio:upgrade. - Optional:
composer install --no-dev --optimize-autoloader. Novendor/replacement is needed.
From 1.3.0 onwards you can upload the ZIP on the same screen.
Site in public_html with travelio-path.php (cPanel Option B)? The ZIP upload already puts the new public/ files into public_html. For a manual copy, copy WebSourceCode/public/* into public_html/ and everything else into the application folder; keep travelio-path.php.
| From → to | What changes | Do this |
|---|---|---|
| 1.0.x → 1.7.0 (everything below, in one jump) | Promo codes, loyalty redemption, verified reviews; wallet top-up endpoint removed; visa documents moved to private storage. | Finalize does the migration and the visa-document move; then open Settings → Loyalty & Referrals and Booking Policy. The migration backfills gross_amount and grants the new review_* permissions to existing staff. |
| 1.1 → 1.4.0 (part of 1.7.0) | Search-widget tabs and traveler options, flight return leg, hotel provider fields, home page sections, company bank accounts, payment proofs, payment_status / supplier_status on bookings, JazzCash and Easypaisa. Existing bookings are backfilled (supplier status not_required). | Migrate. Add bank accounts under Tour Management → Bank Accounts; enter any new gateway keys. No role changes — Payment Verification and Bank Accounts reuse the Booking/Accounting permissions. |
| Marketplace tables (part of 1.4.0) | Tables vendors, vendor_earnings, vendor_payouts, vendor_documents; vendor_id + vendor_approval on packages, hotels, transport services, Hajj packages and visa services; vendor_id on transport bookings; payment fields on visa applications; ledger accounts 2400 Vendor Payable and 5500 Vendor Cost of Sales; vendor_* permissions and a Vendor role. | Migrate. The marketplace stays off until you switch it on in Settings → General. Existing listings default to approved and vendor-less. Run php artisan accounting:rebuild only if you want the ledger re-derived. |
| 1.4.0 → 1.5.0 | Settings → Modules (switch whole services off) and the agent-registration switch; six flight APIs; Featured tour and Hajj/Umrah packages (is_featured); Promotions (table promotions) with the new home sections Promotions and Hajj & Umrah packages; vendor settlement controls, commission VAT, auto-pay and vendor documents. | Finalize. Then review Settings → Modules (every module starts on), enter credentials for the flight APIs you use under Settings → Flight API, and rebuild the mobile app with 1.5.0 for the new screens. |
| 1.5.0 → 1.6.0 | Live chat (tables chat_conversations, chat_messages); the Messenger setting; Appearance fonts, heading font, text size and the Admin Panel Appearance section; website search filters and live hotel rates; installer and security hardening. Customers who already finished sign-up are marked email-verified. | Finalize. Live chat starts off on an upgraded site — switch it on in Settings → Live chat settings when you are ready. The server needs PHP 8.3+. Rebuild the mobile app with 1.6.0 for live chat; 1.5.0 apps keep working. |
| 1.6.0 → 1.7.0 | Stripe, PayPal and Twilio settings (off); exchange rates on currencies and conversion fields on payment_intents; two-factor columns on users; agent groups, credit limit, staff logins and applications (table agent_groups, new users columns); webhooks (tables webhooks, webhook_deliveries); hotel rate calendar (table hotel_room_rates); activities (tables activities, activity_options, activity_slots, activity_bookings) and the activity_* permissions for the admin roles. | Finalize runs the migrations, grants the new permissions and adds the new settings switched off. Hotels price as before until you set nights in the rate calendar. Make sure the cron entry is in place so webhooks are sent. Rebuild the app to get the 1.7.0 screens (older apps keep working). |
After upgrading, run php artisan optimize:clear, hard-refresh the browser, and rebuild the mobile app only if the app release notes say so — the API is backwards-compatible (status = paid still works and mirrors payment_status).
Go-Live Checklist
| Check | |
|---|---|
| ☐ | APP_DEBUG=false, APP_ENV=production, APP_DEMO=false in .env |
| ☐ | APP_URL matches the real domain, with https; public/uploads writable |
| ☐ | Every demo account password changed, unused demo accounts deleted |
| ☐ | SSL certificate installed and http redirects to https |
| ☐ | Mail configured and a test email actually received (sign-up codes depend on it) |
| ☐ | Scheduler cron installed (no queue worker needed) |
| ☐ | Agency name, logo, favicon, currency and timezone set |
| ☐ | Demo content replaced with your own pages, packages and sliders |
| ☐ | Booking policy (cancellation window and penalty) and loyalty/coupon rules reviewed |
| ☐ | Payment: bank accounts added and a test transfer approved; each online gateway tested in sandbox, taken out of sandbox, one small live payment made; currency matches the gateway |
| ☐ | Optional: social login, push (service-account JSON + app config files), WhatsApp, SMS, Amadeus and OpenAI keys entered and tried |
| ☐ | Settings → API Security reviewed; if you rotated the key, the app was rebuilt with the same APP_API_KEY |
| ☐ | Marketplace deliberately on or off; if on, default commission set and a test vendor approved end to end |
| ☐ | A database backup taken and downloaded off the server (there is no built-in backup) |
| ☐ | Mobile app rebuilt against the live API_HOST, with your own package ID |
| ☐ | php artisan optimize run after the final .env change |
Social Login
Lets customers sign in with Google or Facebook instead of a password. Configure both in Settings → Social Login Settings; each provider has its own form, an Active switch, and is offered only while active. Admin and staff login remains password-only.
https://yourdomain.com/auth/social/google/callbackhttps://yourdomain.com/auth/social/facebook/callbackGoogle: create an OAuth Web client in Google Cloud Console and add the redirect URI above. Facebook: create an app at developers.facebook.com, add Facebook Login and put the redirect URI in Valid OAuth Redirect URIs.
Mobile app: the app signs in with the native Google/Facebook SDK and posts the credential to
POST /api/v1/auth/social/google(id_token) or/auth/social/facebook; the backend verifies it with Google/Facebook, checks the audience against the client IDs you saved (web, Android or iOS) and returns the normal API token. Google must report a verified email.GET /api/v1/auth/social/providerstells the app which buttons to show.The redirect URI must match exactly, including https and with no trailing slash. A saved secret is never shown again; leave the field blank to keep it.