API reference — Sales-Manager, Shopper-Admin, Public & CRM-webhooks
This page documents the four remaining backend personas that are not the big shopper/broker/sales-man surfaces:
- Sales-Manager — analytics-only backend for the sales-manager dashboard (16 routes).
- Shopper-Admin — the analytics backend that powers the Shopper-Analytics app (35 routes).
- Public & auth — the shared, mostly-unauthenticated surface: login, currency, payment-gateway callbacks, metrics (11 routes).
- CRM-middleware inbound webhooks — the routes the
crms-middlewareservice calls back into this backend (10 routes).
Every route below is verified against backend/routes/api/*.php + the controllers (cited file:line). Two auth facts apply everywhere:
- All routes sit inside Laravel’s
apimiddleware group, so responses are wrapped in a{ "data": …, "message": … }envelope byFormatApiResponse— except routes markedwithoutMiddleware([FormatApiResponse::class])(the payment callbacks and Prometheus scrape). throttle:apiallows 60 req/min per user-id-or-IP, but 180 req/min for any path containing/analytics(RouteServiceProvider.php:129-137). That higher limit covers almost every Sales-Manager and Shopper-Admin route.
Sales-Manager API
Section titled “Sales-Manager API”Route file: backend/routes/api/sales-manager.php · URL prefix: api/sales-manager · Controller namespace: App\Http\Controllers\Api\SalesManager.
Every route is guarded by all three of: user-provider:sales_managers (rebinds the web guard to the SalesManager model), auth:sanctum (valid bearer token), and ability:access-sales-manager-api (AuthTokenAbilities::ACCESS_SALES_MANAGER_API, string access-sales-manager-api). There is no login route here — sales-managers authenticate through the shared public api/login (see the Public section and the gotcha below).
Paths in the table are relative to the api/sales-manager prefix.
| Method | Path | Purpose | Notes (controller) |
|---|---|---|---|
| POST | logout |
Revoke the current Sanctum token | Auth\LogoutController |
| GET | analytics/filters/destinations |
Filter options — destinations | Analytics\FiltersController@getDestinations |
| GET | analytics/filters/unit-types |
Filter options — unit types | Analytics\FiltersController@getUnitTypes |
| GET | analytics/filters/brokerage-companies |
Filter options — brokerage companies | Analytics\FiltersController@getBrokerageCompanies |
| GET | analytics/filters/assigned-sales-managers |
Filter options — sales managers assigned to this manager | Analytics\FiltersController@getSalesManagers |
| GET | analytics/filters/assigned-sales-managers-titles |
Filter options — assigned sales-manager titles | Analytics\FiltersController@getSalesManagersTitles |
| GET | analytics/leads/lifecycle-counts |
Lead counts per lifecycle stage | Analytics\LeadController@lifecycleCounts |
| GET | analytics/commissions |
Commission totals for the manager | Analytics\SalesInvoicesController@getCommissionData |
| GET | analytics/brokers-commissions |
Per-broker commission breakdown | Analytics\SalesInvoicesController@getBrokersCommissions |
| GET | analytics/brokers-commissions/export |
Export broker commissions (file) | Analytics\SalesInvoicesController@exportBrokersCommissions |
| GET | analytics/brokerages-ranking |
Ranking of brokerage companies | Analytics\SalesController@getBrokeragesRanking |
| GET | analytics/sales-managers-ranking |
Ranking of sales managers | Analytics\SalesController@getSalesManagersRanking |
| GET | analytics/brokers-ranking |
Ranking of brokers | Analytics\SalesController@getBrokersRanking |
| GET | analytics/revenue-insights |
Revenue insight aggregates | Analytics\SalesController@getRevenueInsights |
| GET | analytics/sales/count-per-destination |
Sales count grouped by destination | Analytics\SalesController@countPerDestination |
| GET | analytics/sales/international |
International sales figures | Analytics\SalesController@getInternationalSales |
Shopper-Admin API (analytics backend)
Section titled “Shopper-Admin API (analytics backend)”Route file: backend/routes/api/shopper-admin.php · URL prefix: api/shopper-admin · Controller namespace: App\Http\Controllers\Api\ShopperAdmin.
This persona is the backend for the Shopper-Analytics app — the internal dashboard that reports on shopper behaviour, sales, leads, demographics and Google-Analytics page views. Everything below sits under user-provider:shopper_admins. The three auth routes are public; the rest additionally require auth:sanctum + ability:access-shopper-analytics-api (AuthTokenAbilities::ACCESS_SHOPPER_ANALYTICS_API, string access-shopper-analytics-api).
Several endpoints read from Google Analytics (GA Data API via Spatie\Analytics) rather than the local DB — recognisable by the ggl / views / page-views names (e.g. analytics/destinations-ggl-views, analytics/customer-behavior/ggl-most-views-breakdown, analytics/demographics/ggl-page-views-per-country). The sales/leads/requests endpoints aggregate the local mirror tables.
Paths are relative to the api/shopper-admin prefix.
Auth (public)
Section titled “Auth (public)”| Method | Path | Purpose | Notes (controller) |
|---|---|---|---|
| POST | login |
Email + password login; mints a Sanctum token with the access-shopper-analytics-api ability |
Auth\LoginController (ShopperAdmin/Auth/LoginController.php:44-49) |
| POST | forgot-password |
Send password-reset email | Auth\ResetPasswordController@forgotPassword |
| POST | reset-password |
Reset password via token | Auth\ResetPasswordController@resetPassword |
Authenticated (auth:sanctum + ability:access-shopper-analytics-api)
Section titled “Authenticated (auth:sanctum + ability:access-shopper-analytics-api)”| Method | Path | Purpose | Notes (controller) |
|---|---|---|---|
| POST | logout |
Revoke the current Sanctum token | Auth\LogoutController |
| GET | me |
Return the authenticated shopper-admin | ShopperAdminController@show |
| GET | analytics/destinations |
Published destinations (for the dashboard) | FiltersController@getPublishedDestinations |
| GET | analytics/filters/destinations |
Filter options — destinations | FiltersController@getDestinations |
| GET | analytics/filters/unit-types |
Filter options — unit types | FiltersController@getUnitTypes |
| GET | analytics/overview-summary |
Sold-units, revenue, and request totals | UnitSaleController@getOverviewSummary |
| GET | analytics/overview-summary/users-purchase |
Users → purchase conversion rate | UnitSaleController@getUserPurchaseConversionRate |
| GET | analytics/overview-summary/users-interest-conversion |
Users → interest conversion rate | UnitSaleController@getUserInterestConversionRate |
| GET | analytics/destinations-performance |
Performance metrics per destination | UnitSaleController@getDestinationsPerformance |
| GET | analytics/destinations-ggl-views |
Google-Analytics views per destination | UnitSaleController@getDestinationsGGLViews |
| GET | analytics/unit-types-performance |
Performance metrics per unit type | UnitSaleController@getUnitTypesPerformance |
| GET | analytics/user-requests/count |
Count of user requests | UserRequestController@countUserRequests |
| GET | analytics/unit-services/count-per-service-type |
Service-request counts per service type | UserRequestController@countServiceRequestsPerServiceType |
| GET | analytics/logins/sessions-breakdown |
Login session breakdown | ShopperController@getLoginSessions |
| GET | analytics/sales/performance-breakdown |
Sales performance over time | UnitSaleController@getSalesPerformance |
| GET | analytics/sales/payment-breakdown |
Sales split by payment method | UnitSaleController@getPaymentBreakdown |
| GET | analytics/sales/currency-breakdown |
Sales split by currency | UnitSaleController@getCurrencyBreakdown |
| GET | analytics/sales/top-performers |
Top-performing sales entities | UnitSaleController@getSalesTopPerformers |
| GET | analytics/sales/direct-vs-indirect-purchase |
Direct vs indirect sales counts | UnitSaleController@countDirectVsInDirectSales |
| GET | analytics/leads/performance |
Lead performance metrics | LeadController@getLeadsPerformance |
| GET | analytics/leads/count-per-destinations |
Lead counts per destination | LeadController@countLeadsPerDestination |
| GET | analytics/sales/count-per-unit-types-breakdown |
Sales counts per unit-type | UnitSaleController@countPerUnitTypeBreakdown |
| GET | analytics/leads/sales-count-per-destinations |
Leads vs sales counts per destination | LeadController@countLeadsVsSalesPerDestination |
| GET | analytics/sales/sales-revenues-per-unit-types |
Revenue per unit type | UnitSaleController@getSalesRevenuesByUnitType |
| GET | analytics/customer-behavior/ggl-most-views-breakdown |
Google-Analytics most-viewed breakdown | CustomerBehaviorController@getMostViewsBreakdown |
| GET | analytics/customer-behavior/search-with-no-results |
Searches that returned no results | CustomerBehaviorController@getSearchWithNoResults |
| GET | analytics/customer-behavior/search-with-no-results/export |
Export no-result searches (file) | CustomerBehaviorController@exportSearchWithNoResult |
| GET | analytics/customer-behavior/most-searches-filters |
Most-used search filters | CustomerBehaviorController@getMostSearchesFilters |
| GET | analytics/demographics/ggl-page-views-per-country |
Google-Analytics page views per country | DemographicsController@getPageViewsPerCountry |
| GET | analytics/funnel-analysis/online-offline-sales |
Online vs offline sales funnel | FunnelAnalysisController@getFunnelAnalysis |
| GET | analytics/demographics/sales-per-country |
Sales per country code | DemographicsController@getSalesByCountryCode |
| GET | analytics/demographics/leads-per-country |
Leads per country code | DemographicsController@getLeadsByCountryCode |
Public & auth API
Section titled “Public & auth API”Route file: backend/routes/api.php · URL prefix: api. No persona namespace — controllers are fully qualified.
This is the shared surface: the cross-persona login, the authenticated-user helpers, currency lookups, payment-gateway callbacks, and the Prometheus scrape.
| Method | Path | Purpose | Notes (controller / middleware) |
|---|---|---|---|
| POST | api/login |
Broker and sales-manager email + password login; branches on role_id and mints a Sanctum token with the matching ability |
Api\Auth\LoginController (LoginController.php:28-102) |
| GET | api/user |
Return the authenticated user | Api\Auth\UserController@show · auth:sanctum |
| PUT | api/user/complete-walkthrough |
Mark the onboarding walkthrough complete | Api\Auth\UserController@completeWalkThrough · auth:sanctum |
| POST | api/paymob/callback |
Paymob server-to-server transaction callback | Payment\CallbackController@paymob · withoutMiddleware([FormatApiResponse]) |
| GET | api/paymob/callback |
Paymob browser response/redirect callback | Payment\CallbackController@paymobResponseCallback · withoutMiddleware([FormatApiResponse]) |
| GET | api/thawani/callback/{status} |
Thawani callback ({status} = success / cancel / fail) |
Payment\CallbackController@thawani · withoutMiddleware([FormatApiResponse]) |
| GET | api/wspg/callback/{status} |
WSPG (bank gateway) callback ({status} = success / cancel / fail) |
Payment\CallbackController@wspg · withoutMiddleware([FormatApiResponse]) |
| GET | api/currencies |
List currencies for a country (country_id or country_slug) |
Api\Currency\CurrencyController |
| GET | api/currency-rates |
FX conversion rates (fetched from the external rates API, cached) | Api\Currency\CurrencyRatesController |
| GET | api/egp-exchange-rate |
EGP exchange rate, proxied from the CRM middleware ({base}/egp-exchange-rate) |
Api\Currency\EgpExchangeRateController |
| GET | api/prometheus/metrics |
Prometheus metrics scrape (manual bearer check inside) | PrometheusMetricScrapeController@scrapeMetrics · withoutMiddleware([FormatApiResponse]) |
The three payment callbacks and the metrics scrape return raw output (redirects / plain text), which is why they opt out of the { data, message } envelope. The login, user, and currency routes return the wrapped envelope.
CRM-middleware inbound webhooks
Section titled “CRM-middleware inbound webhooks”Route file: backend/routes/api/crms-middleware.php · URL prefix: api/crms-middleware · Controller namespace: App\Http\Controllers\Api\CrmsMiddleware.
These are the routes the external crms-middleware service POSTs back into this backend — the inbound half of the CRM integration (the outbound half is the CrmMiddlewareClient calls documented on the other persona pages). They keep the local mirror in sync (units, brokers, sales, EOIs) and fan out notifications when CRM-side events happen.
How the middleware authenticates (Passport client_credentials):
- The middleware first calls
POST api/crms-middleware/loginwith itsclient_id+client_secret. That route (AuthController@login) is not behind theclientguard — it proxies an internalPOST {config('app.url')}/oauth/tokenwithgrant_type=client_credentialsandscope=middleware-webhook, returningaccess_token,expires_in,token_type(AuthController.php:12-31). - Every other route is guarded by the
clientmiddleware = Laravel Passport’sCheckClientCredentials(Kernel.php:81). The middleware sends the token from step 1 asAuthorization: Bearer <access_token>; there is no per-user Sanctum token and no persona provider here.
Paths are relative to the api/crms-middleware prefix.
| Method | Path | Purpose | Notes (controller) |
|---|---|---|---|
| POST | login |
Exchange client_id + client_secret for a client_credentials token (scope middleware-webhook) — no client guard |
AuthController@login |
| POST | units/{sourceId} |
Upsert a single unit from the CRM (transform + load into the local mirror) | UnitController@insert |
| POST | brokers/register |
Create a broker locally from a CRM registration (company, manager, commissions, destination access) | BrokerController@register |
| PATCH | brokers/update |
Update an existing broker from the CRM | BrokerController@update |
| POST | brokers/user/notifications |
Push a general notification to a broker (user_id + message) |
NotificationController@generalNotification |
| POST | leads/{leadSourceId}/notifications |
Notify the owning broker of a lead event | NotificationController@leadNotification |
| POST | meetings/{meetingSourceId}/notifications |
Notify the owning broker of a meeting event | NotificationController@meetingNotification |
| POST | eois/{eoiSourceId}/notifications |
Save an EOI from CRM to portal and notify the user | NotificationController@eoiNotification |
| PATCH | sales/{saleSourceId} |
Update a local sale mirror from the CRM | UnitSaleController@updateSale |
| POST | eoi |
Create/resolve an EOI from the CRM (notifies the user; emails a payment link when pending payment) | EOIController@store |
Gotchas
Section titled “Gotchas”- Sales-Manager has no
login/forgot-password/reset-passwordroutes. It reuses the sharedPOST api/login, which is the same endpoint brokers use — the controller branches on the user’srole_idand hands out the right ability. Shopper-Admin, in contrast, ships its own login + password-reset routes underapi/shopper-admin. - Two logins for one email+password model. Broker/Sales-Manager go through
api/login(Api\Auth\LoginController); Shopper-Admin goes throughapi/shopper-admin/login(ShopperAdmin\Auth\LoginController). They mint tokens with different abilities and against different models, so a token from one will fail the other’sability:gate. - Provider slugs are underscored plurals. The route files use
user-provider:sales_managersanduser-provider:shopper_admins(not the hyphenatedsales-manager/shopper-adminshorthand seen in prose). The prefix stays hyphenated (api/sales-manager,api/shopper-admin). - The
/analyticsthrottle is per-path, not per-persona. Any request whose path contains/analyticsgets 180 req/min; everything else gets 60. Because nearly every Sales-Manager and Shopper-Admin route containsanalytics, they run at the higher limit — but the Shopper-Adminmeandlogoutroutes (noanalyticssegment) fall back to 60. - CRM-middleware
loginsits outside its own guard. The token-minting route is deliberately un-guarded (it is the auth handshake); the other nine webhooks require the resulting Passportclient_credentialsbearer. Don’t movelogininside theclientgroup — it would become a chicken-and-egg lockout. - Shopper-Admin GA-backed endpoints depend on Google Analytics, not the DB. The
ggl/page-viewsendpoints call the GA Data API viaSpatie\Analytics; a GA outage or missing credentials degrades those specific analytics cards while the DB-backed sales/leads cards keep working. egp-exchange-rateis a live CRM proxy. Unlikecurrencies(local DB) andcurrency-rates(external rates API, cached),api/egp-exchange-ratereaches through thecrms-middlewareboundary ({base}/egp-exchange-rate) on every call, so it inherits the middleware’s latency/availability.
