Skip to content

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-middleware service 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 api middleware group, so responses are wrapped in a { "data": …, "message": … } envelope by FormatApiResponseexcept routes marked withoutMiddleware([FormatApiResponse::class]) (the payment callbacks and Prometheus scrape).
  • throttle:api allows 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.

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

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.

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

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.


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):

  1. The middleware first calls POST api/crms-middleware/login with its client_id + client_secret. That route (AuthController@login) is not behind the client guard — it proxies an internal POST {config('app.url')}/oauth/token with grant_type=client_credentials and scope=middleware-webhook, returning access_token, expires_in, token_type (AuthController.php:12-31).
  2. Every other route is guarded by the client middleware = Laravel Passport’s CheckClientCredentials (Kernel.php:81). The middleware sends the token from step 1 as Authorization: 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

  • Sales-Manager has no login / forgot-password / reset-password routes. It reuses the shared POST api/login, which is the same endpoint brokers use — the controller branches on the user’s role_id and hands out the right ability. Shopper-Admin, in contrast, ships its own login + password-reset routes under api/shopper-admin.
  • Two logins for one email+password model. Broker/Sales-Manager go through api/login (Api\Auth\LoginController); Shopper-Admin goes through api/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’s ability: gate.
  • Provider slugs are underscored plurals. The route files use user-provider:sales_managers and user-provider:shopper_admins (not the hyphenated sales-manager / shopper-admin shorthand seen in prose). The prefix stays hyphenated (api/sales-manager, api/shopper-admin).
  • The /analytics throttle is per-path, not per-persona. Any request whose path contains /analytics gets 180 req/min; everything else gets 60. Because nearly every Sales-Manager and Shopper-Admin route contains analytics, they run at the higher limit — but the Shopper-Admin me and logout routes (no analytics segment) fall back to 60.
  • CRM-middleware login sits outside its own guard. The token-minting route is deliberately un-guarded (it is the auth handshake); the other nine webhooks require the resulting Passport client_credentials bearer. Don’t move login inside the client group — it would become a chicken-and-egg lockout.
  • Shopper-Admin GA-backed endpoints depend on Google Analytics, not the DB. The ggl / page-views endpoints call the GA Data API via Spatie\Analytics; a GA outage or missing credentials degrades those specific analytics cards while the DB-backed sales/leads cards keep working.
  • egp-exchange-rate is a live CRM proxy. Unlike currencies (local DB) and currency-rates (external rates API, cached), api/egp-exchange-rate reaches through the crms-middleware boundary ({base}/egp-exchange-rate) on every call, so it inherits the middleware’s latency/availability.