Broker API reference
The Broker persona is served from routes/api/broker.php (114 routes). It is the estate-agent-facing
surface: brokers browse published inventory, submit leads and EOIs on behalf of clients, book meetings,
claim sales commissions, and (for the Oman market) run a document-driven registration workflow. This page
is exhaustive — every route in the file is in one of the tables below, plus the shared login route that
brokers use. Cited line numbers are backend/… paths.
Base, auth, and the envelope
Section titled “Base, auth, and the envelope”Base URL. All routes are mounted under the api/broker prefix on the backend host
(RouteServiceProvider.php:152-155). Staging is https://api.orascom.robustastudio.com, so a full URL
looks like https://api.orascom.robustastudio.com/api/broker/leads.
Auth guard. The whole file is wrapped in user-provider:brokers (broker.php:23), which rebinds the
web guard + password broker to the Broker model. Two nested authenticated groups sit inside it:
- Main broker API —
auth:sanctum+ability:access-broker-api(broker.php:79). Covers leads, meetings, invoices, EOI, units, analytics, launches, notifications, and the post-approval Oman application block. - Oman pre-registration —
auth:sanctum+ability:access-pre-registration-broker-api(broker.php:50-53). A separate token/ability: a main-API token is rejected here and vice-versa.
Login is email + password, not OTP. Brokers authenticate through the shared route
POST api/login (routes/api.php, Api\Auth\LoginController) with email + password. That route lives
outside the api/broker prefix and is shared with sales-managers; the controller detects the role and
returns a Broker or SalesManager resource. OTP in the broker file is onboarding-only — it gates
new-broker registration (otp/verify, otp/resend-otp) and the Oman pre-registration flow. There is no
OTP login for an existing broker.
Response envelope. The api middleware group wraps every response in { data, message } via
FormatApiResponse. Routes marked withoutMiddleware([FormatApiResponse::class]) (all file/PDF downloads)
return the raw body instead.
Named throttles used in this file (AppServiceProvider.php:44-85): verify-otp-email and
send-otp-email are 3/min keyed by the email field; sales-offer-generate defaults to 5/min keyed by
user-id or IP. The api group also applies 60/min globally, raised to 180/min on any /analytics path.
Auth & onboarding
Section titled “Auth & onboarding”The public block (no token; user-provider:brokers only) plus the whitelist helpers. register calls the
CRM; the rest are local DB + mail.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| POST | api/login |
Broker (or sales-manager) email + password login | Public · shared route, not under api/broker · EmailLoginRequest |
| POST | api/broker/forgot-password |
Send broker password-reset email | Public · ForgotPasswordRequest (ResetPasswordController.php:16) |
| POST | api/broker/reset-password |
Reset password via emailed token | Public · ResetPasswordRequest (:25) |
| POST | api/broker/set-new-password |
Set password for the first time | Public · SetNewPasswordRequest (:34) |
| GET | api/broker/broker-domains |
List allowed broker email domains | Public · UserController@getBrokerDomains (:54) |
| POST | api/broker/register |
Register a new broker in the CRM | Public · CreateBrokerRequest (:63) · POST to CRM |
| POST | api/broker/otp/verify |
Verify registration OTP (email) | Public · throttle:verify-otp-email · VerifyOtpViaEmailRequest |
| POST | api/broker/otp/resend-otp |
Resend registration OTP (email) | Public · throttle:send-otp-email · ResendOtpViaEmailRequest |
| POST | api/broker/import-whitelisted-emails |
Bulk-import whitelisted broker emails | Public · ImportWhitelistedEmailsRequest (:101) |
| GET | api/broker/check-whitelisted-emails |
Check whether an email is whitelisted | Public · CheckWhitelistedEmailsExistenceRequest (:116) |
| POST | api/broker/logout |
Revoke the current access token | Sanctum + access-broker-api (LogoutController) |
Units, filters, whitelabel & saved units
Section titled “Units, filters, whitelabel & saved units”Catalog browse for the broker’s assigned inventory, plus the co-branding logo and saved-unit list.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| POST | api/broker/whitelabel/logo |
Upload broker whitelabel logo | StoreWhitelabelLogoRequest (WhitelabelController.php:14) |
| DELETE | api/broker/whitelabel/logo |
Remove whitelabel logo | WhitelabelController@destroyLogo (:23) |
| GET | api/broker/filters |
Available unit filters for the broker | FilterController |
| GET | api/broker/units |
List units (filtered) | IndexForBrokerRequest (UnitController.php:19) |
| GET | api/broker/units/compare |
Compare units by ids | CompareRequest |
| GET | api/broker/units/compare/export |
Export compared units to xlsx | CompareExportRequest · per-unit CRM payment-terms |
| GET | api/broker/user/saved-units |
List saved units | SavedUnitRequest |
| POST | api/broker/user/saved-units/{publishedAvailableUnitBroker} |
Save a unit | resource-visibility:publishedAvailableUnitBroker |
| DELETE | api/broker/user/saved-units/{savedUnit} |
Remove a saved unit | resource-visibility:savedUnit |
| GET | api/broker/units/{publishedUnitBroker} |
Show unit detail | resource-visibility:publishedUnitBroker |
| POST | api/broker/units/{publishedUnitBroker}/share-reservation-link |
Email + SMS a reservation link to a lead | ShareReservationLinkRequest (:39) |
| GET | api/broker/units/{publishedUnitBroker}/payment-terms |
Fetch unit payment terms from the CRM | resource-visibility:publishedUnitBroker |
| GET | api/broker/documents |
Broker documents list | Mocked stub (UserController@getDocuments) |
Units — sales offer PDF
Section titled “Units — sales offer PDF”The sales-offer PDF is generated asynchronously by a job that calls the PDF microservice; the client polls a status endpoint and then follows a signed download URL. See the request/response detail below.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/units/{publishedUnitBroker}/sales-offer |
Fetch the CRM-rendered sales-offer PDF | resource-visibility:publishedUnitBroker |
| GET | api/broker/units/{publishedUnitBroker}/sales-offer/{paymentPlanId} |
Legacy inline PDF | Broken — controller method missing, see Gotchas |
| POST | api/broker/units/{publishedUnitBroker}/sales-offer/{paymentPlanId}/generate |
Start async sales-offer PDF generation | throttle:sales-offer-generate · GenerateRequest |
| GET | api/broker/units/{publishedUnitBroker}/sales-offer/{salesOfferPdfGeneration}/status |
Poll generation status + file URL | showGeneration (SalesOfferPdfController.php:61) |
| GET | api/broker/units/{publishedUnitBroker}/sales-offer/{salesOfferPdfGeneration}/download |
Stream the generated PDF | signed URL · raw body (no envelope) · broker.php:74 |
Leads & deals
Section titled “Leads & deals”Leads are CRM-backed. submitLead first checks the monthly per-destination cap, then POSTs to the CRM.
List endpoints fan out over the broker’s per-country source ids and split into active vs past deals.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/leads |
List broker leads (active + past deals) | LeadIndexRequest (LeadController.php:105) |
| GET | api/broker/leads/count |
Monthly leads count + cap for a destination | CountLeadRequest (:78) |
| POST | api/broker/leads |
Submit a new lead | SubmitLeadRequest (:29) · cap-checked |
| GET | api/broker/leads/{leadSourceId} |
Lead details | DetailsRequest (:116) |
| GET | api/broker/leads-duplicate |
List duplicate leads | LeadIndexRequest (:94) |
Meetings
Section titled “Meetings”| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/meetings |
List all broker meetings | MeetingController@getMeetings (:20) |
| GET | api/broker/leads/{leadSourceId}/meetings |
List meetings for a lead | IndexRequest (:55) |
| POST | api/broker/leads/{leadSourceId}/meetings |
Submit a meeting for a lead | SubmitLeadMeetingRequest (:89) |
Sales invoices & commission
Section titled “Sales invoices & commission”The broker uploads a signed contract PDF to claim commission on a sale. The controller re-derives the commission from the CRM and enforces submission rules before creating the invoice.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/leads/{leadSourceId}/sales_invoices |
List sales invoices for a lead | PaginationRequest (SalesInvoiceController.php:25) |
| POST | api/broker/leads/{leadSourceId}/sales_invoices |
Create a sales invoice (claim commission) | CreateLeadSalesInvoiceRequest (:54) · gated on can_submit_invoice |
Analytics
Section titled “Analytics”Read-only dashboards. All accept BrokersFiltersRequest / BaseFiltersRequest and are served from local
mirror tables (no request-time CRM call). The /analytics path raises the rate limit to 180/min.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/analytics/revenue-insights |
Revenue insights | SalesController@getUnitsData |
| GET | api/broker/analytics/commissions |
Commission analytics data | SalesInvoicesController@getCommissionsData |
| GET | api/broker/analytics/company-ranking |
Company commission ranking | getCompanyRanking |
| GET | api/broker/analytics/commissions/statistics |
Commission statistics | getCommissionStatistics |
| GET | api/broker/analytics/brokers-ranking |
Brokers sales ranking | getBrokersRanking |
| GET | api/broker/analytics/leads/lifecycle-counts |
Lead lifecycle stage counts | Analytics\LeadController@lifecycleCounts |
| GET | api/broker/analytics/sales/count-per-destination |
Sales count per destination | countPerDestination |
| GET | api/broker/analytics/sales/count-per-unit-types-breakdown |
Daily sales per unit type | countPerUnitTypeBreakdown |
| GET | api/broker/analytics/filters/assigned-brokers |
Assigned-brokers filter values | FiltersController@getAssignedBrokers |
| GET | api/broker/analytics/filters/destinations |
Destinations filter values | getDestinations |
| GET | api/broker/analytics/filters/unit-types |
Unit-types filter values | getUnitTypes |
Destinations & news
Section titled “Destinations & news”Both are gated per-assignment. Destination sub-resources share the resource-visibility:destinationSlug
group; news/{newsSlug} is gated by resource-visibility:newsSlug.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/destinations |
List assigned published destinations | DestinationController@index |
| GET | api/broker/destinations/{destinationSlug} |
Destination detail | resource-visibility:destinationSlug |
| GET | api/broker/destinations/{destinationSlug}/unit-types |
Destination unit types | visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/projects |
Allowed destination projects | visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/locations |
Destination locations | visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/testimonials |
Destination testimonials | visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/facilities |
Destination facilities | visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/faqs |
Destination FAQs | PaginationRequest · visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/construction-updates |
Construction updates | visibility-gated |
| GET | api/broker/destinations/{destinationSlug}/educational-hubs |
Educational hubs | visibility-gated |
| GET | api/broker/news |
List news (filtered) | IndexRequest (NewsController.php:14) |
| GET | api/broker/news/{newsSlug} |
Show a news article | resource-visibility:newsSlug |
Launches
Section titled “Launches”Launch catalog for the broker. Bound by launchValue (slug or id, published-only) or publishedLaunch.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/launches |
List broker launches | LaunchesController@index |
| GET | api/broker/launches/{launchValue} |
Show launch detail | binding launchValue |
| GET | api/broker/launches/{launchValue}/facilities |
List launch facilities | binding launchValue |
| GET | api/broker/launches/{launchValue}/faqs |
List launch FAQs | PaginationRequest |
| GET | api/broker/launches/{publishedLaunch}/unit_types/{priceSourceId}/preferences |
Launch unit-type preferences | CRM-backed |
| GET | api/broker/bank-accounts |
List bank accounts | BankAccountsController@index |
EOI (Expression of Interest)
Section titled “EOI (Expression of Interest)”The EOI tree is the heaviest broker domain: list/search/export, an async full-EOI submit, per-EOI
lifecycle actions, and a nested product to unit-type to preference builder. Most write actions call the CRM
directly; storeAll is dispatched to a queued job (SaveEOIToCrmJob).
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/eois |
List EOIs | ListEOIRequest (EOIController.php:50) |
| GET | api/broker/eois/search |
Search EOIs | SearchRequest (:81) |
| GET | api/broker/eois/export |
Export EOIs report (xlsx/csv) | FiltersRequest (:441) |
| POST | api/broker/launches/{publishedLaunch}/eois |
Submit a full EOI (async) | SubmitFullEOIRequest · SaveEOIToCrmJob |
| PATCH | api/broker/launches/{publishedLaunch}/eois/{eoiBySourceId} |
Update an EOI | UpdateEOIRequest (:161) |
| PATCH | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/require-action |
Flag EOI require-action | EOIController@requireAction (:210) |
| PATCH | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/cancel |
Cancel an EOI | CancelEOIRequest (:307) |
| GET | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId} |
Show EOI detail | EOIController@show (:240) |
| GET | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/export |
Export a single EOI as PDF | FiltersRequest (:270) |
| GET | api/broker/launches/{publishedLaunch}/eois/{eoiBySourceId}/resend-payment-link |
Resend the EOI payment-link email | local mail (:374) |
| POST | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/attach |
Attach a file to an EOI | AttachRequest (EOIAttachmentController.php:42) |
| GET | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/attachments |
List EOI attachments | EOIAttachmentController@index (:21) |
| DELETE | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/attachments/{attachmentSourceId} |
Delete an EOI attachment | EOIAttachmentController@delete (:78) |
| POST | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/products |
Create an EOI product | CreateProductRequest (EOIProductController.php:36) |
| PATCH | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId} |
Update an EOI product | EditProductRequest (:203) |
| PATCH | api/broker/launches/{publishedLaunch}/eois/{eoiBySourceId}/products/{productSourceId}/delete |
Delete a product + sync payment | UpdatePaymentRequest (:150) |
| POST | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId}/unit-types |
Add a product unit type | CreateUnitTypeRequest (:72) |
| PATCH | api/broker/launches/{publishedLaunch}/eois/{eoiBySourceId}/products/{productId}/unit-types/{unitTypeId}/delete |
Delete a unit type + sync payment | UpdatePaymentRequest (:249) |
| POST | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId}/unit-types/{unitTypeSourceId}/preferences |
Add a unit-type preference | CreatePreferenceRequest (:110) |
| DELETE | api/broker/launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId}/unit-types/{unitTypeSourceId}/preferences/{prefId} |
Delete a preference | EOIProductController@deletePreference (:304) |
| POST | api/broker/launches/{publishedLaunch}/eois/{eoiBySourceId}/products/full-product |
Create a full product tree | CreateFullProductRequest (:351) |
| POST | api/broker/launches/{publishedLaunch}/eois/{eoiBySourceId}/products/{productSourceId}/unit-types/store-bulk |
Bulk-create product unit types | CreateProductUnitTypesRequest (:398) |
Notifications
Section titled “Notifications”| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/user/notifications |
List broker notifications | IndexRequest (NotificationController.php:14) |
| POST | api/broker/user/notifications/read |
Mark notifications as read | MarkAsReadRequest |
Oman pre-registration
Section titled “Oman pre-registration”A self-contained onboarding sub-flow under oman-pre-registration-applications/…. The public block starts
an application and issues an OTP-verified pre-registration token (access-pre-registration-broker-api);
the authed block walks the application through its status machine and manages documents. This ability is
distinct from the main broker API — a main-API token will not work here.
Public sub-flow (no token):
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| POST | api/broker/oman-pre-registration-applications/start |
Start pre-registration, send OTP | throttle:send-otp-email · StartPreRegistrationRequest |
| POST | api/broker/oman-pre-registration-applications/login |
Login an existing pre-reg broker (sends OTP) | throttle:send-otp-email · LoginPreRegistrationRequest |
| POST | api/broker/oman-pre-registration-applications/otp/verify |
Verify OTP, issue token | throttle:verify-otp-email · VerifyOtpViaEmailRequest |
| POST | api/broker/oman-pre-registration-applications/otp/resend |
Resend the pre-reg OTP | throttle:send-otp-email · ResendOtpViaEmailRequest |
| GET | api/broker/oman-pre-registration-applications/lookups/oman-sales-consultants |
List Oman sales consultants | Public lookup |
| GET | api/broker/oman-pre-registration-applications/lookups/templates |
List pre-registration templates | Public lookup |
Authed sub-flow — auth:sanctum + ability:access-pre-registration-broker-api:
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/oman-pre-registration-applications/me |
Return the authenticated application | PreRegistrationBrokerController@me |
| PATCH | api/broker/oman-pre-registration-applications/ambassador-info |
Update ambassador info (step 1) | UpdateAmbassadorInfoRequest (:26) |
| PATCH | api/broker/oman-pre-registration-applications/company-info |
Update company info (step) | UpdateCompanyInfoRequest (:57) |
| PATCH | api/broker/oman-pre-registration-applications/company-details |
Update company details (step) | UpdateCompanyDetailsRequest (:88) |
| PATCH | api/broker/oman-pre-registration-applications/finalize |
Finalize the application | FinalizeApplicationRequest (:114) |
| PATCH | api/broker/oman-pre-registration-applications/resubmit |
Resubmit after missing info | resubmitApplication (:146) |
| PATCH | api/broker/oman-pre-registration-applications/submit-contract |
Submit the signed contract | submitContract (:178) |
| POST | api/broker/oman-pre-registration-applications/documents |
Upload an application document | CreateBrokerPreRegistrationDocumentRequest (:25) |
| POST | api/broker/oman-pre-registration-applications/documents/signed-contract |
Upload the signed contract | CreateUpdateSignedContractRequest (:52) |
| POST | api/broker/oman-pre-registration-applications/documents/{document} |
Replace a document | UpdateBrokerPreRegistrationDocumentRequest (:83) |
| GET | api/broker/oman-pre-registration-applications/documents |
List application documents | PreRegistrationDocumentController@index (:113) |
| GET | api/broker/oman-pre-registration-applications/documents/{document} |
Download a document file | raw body (no envelope) |
Oman application (post-approval)
Section titled “Oman application (post-approval)”The main-API (access-broker-api) block for an already-registered Oman broker: managing contract/document
resubmissions after the CRM flags them.
| Method | Path | Purpose | Auth / notes |
|---|---|---|---|
| GET | api/broker/oman-application/documents |
List Oman application documents | OmanApplicationDocumentController@index (:23) |
| GET | api/broker/oman-application/documents/{document} |
Download an Oman application document | raw body (no envelope) |
| POST | api/broker/oman-application/documents |
Upload an Oman application document | SaveBrokerApplicationDocumentRequest (:35) |
| POST | api/broker/oman-application/resubmit-contract |
Resubmit signed contracts | ResubmitBrokerApplicationRequest (:19) |
| POST | api/broker/oman-application/resubmit-document |
Resubmit a rejected document | ResubmitBrokerApplicationDocumentRequest (:55) |
Request / response detail
Section titled “Request / response detail”The most important write flows, with real fields from the FormRequest, Action, and Resource. All request
bodies are validated by the FormRequest; unlisted keys are stripped by ->validated().
Broker login — POST api/login
Section titled “Broker login — POST api/login”Email + password. Shared with sales-managers; the controller picks the role and returns the matching
resource. EmailLoginRequest (Auth/EmailLoginRequest.php):
{ "email": "agent@brokerage.com", "password": "secret" }Response — AuthenticationResponse (Api\Auth\LoginController@__invoke): a Bearer token carrying the
access-broker-api ability plus the broker profile. On the first successful login the broker’s
email_verified_at is stamped (LoginController.php:54). Bad credentials throw WrongCredentials.
{ "data": { "user": { "id": 1, "name": "...", "email": "agent@brokerage.com" }, "token": "1|xxxxxxxx", "token_type": "Bearer" }, "message": null}Register a broker — POST api/broker/register
Section titled “Register a broker — POST api/broker/register”Public. CreateBrokerRequest (CreateBrokerRequest.php) — all posted to the CRM via
CreateBrokerFromPortal, hardcoded to the egypt country (UserController.php:74):
{ "salesagent_name": "Jane Agent", "salesagent_email": "jane@brokerage.com", "salesagent_mobile": "+201000000000", "company_source_id": "CRM-COMPANY-123", "is_manager": 0, "manager_email": "boss@brokerage.com", "pc_email": "pc@brokerage.com"}is_manager is a required int; manager_email is nullable; salesagent_email, pc_email, and
company_source_id are required. On a non-200 from the CRM the controller unwraps the CRM message and
returns respondBadRequestWithMsg (:92-96). A successful register is followed by the OTP verify below.
Verify registration OTP — POST api/broker/otp/verify
Section titled “Verify registration OTP — POST api/broker/otp/verify”throttle:verify-otp-email (3/min per email). VerifyOtpViaEmailRequest (Auth/VerifyOtpViaEmailRequest.php):
{ "email": "jane@brokerage.com", "otp": "123456" }VerifyOtp is run for the BROKER_REGISTER action; the consumed OTP row is deleted and the broker’s
registration_phase advances to OTP_VERIFY_DONE (OtpController.php:22-33). Returns an empty
respondSuccess() envelope. otp/resend-otp takes just { "email": "..." } and re-mails the code.
Create a lead — POST api/broker/leads
Section titled “Create a lead — POST api/broker/leads”SubmitLeadRequest (Broker/SubmitLeadRequest.php). Real fields:
{ "customer_inquiry": "<CustomInquiryEnums value>", "destination_slug": "<must exist in destinations.slug>", "first_name": "Ali", "last_name": "Hassan", "mobile_country_code": "+20", "phone": "1000000000", "phone_2": null, "phone_3": null, "email": null, "budget": 500000, "interested_in_unit_id": "<units.id>", "portal_page": "<PortalPages value, optional>", "portal_comments": null, "preferred_communication_method": "<CommunicationMethod value>", "preferred_communication_time": "<CommunicationTime value>"}customer_inquiry, destination_slug, first_name, last_name, mobile_country_code, and phone are
required; budget is numeric, min:0. The controller first calls GetLeadsCount for the destination and,
if create_lead_allowed is false, returns 422 with "You have reached the maximum number of leads for this destination." (LeadController.php:48-50). Otherwise it POSTs to the CRM via SubmitNewLead; a
CRM 200 also persists a local UserRequest of type LEAD. The mapped CRM response is returned as-is.
Submit a sales invoice (claim commission) — POST api/broker/leads/{leadSourceId}/sales_invoices
Section titled “Submit a sales invoice (claim commission) — POST api/broker/leads/{leadSourceId}/sales_invoices”Multipart. CreateLeadSalesInvoiceRequest extends Country\IndexRequest, so it merges the country rules:
pdf_attachment required · file · max:2000 (KB)sale_source_id required · stringcontractual_date nullable · date_format "Y-m-d H:i:s"country_id required_without country_slug · exists countries.id (is_published=1)country_slug required_without country_id · exists countries.slug (is_published=1)Flow (SalesInvoiceController.php:54-118):
- If
broker.can_submit_invoiceis false, return 403"Broker cannot submit invoice". - Look up the
AnalyticsBrokerSalebysource_id = sale_source_idandlead_source_id. If absent, throwAppCustomException("This Sale not exists"). - Enforce
ValidSubmitSecondSalesInvoiceandValidSubmitSalesInvoiceWhenContractualDate. - Re-derive commission from the CRM via
GetCommissionForSale(usingcountry_id), then create the invoice throughCreateLeadSalesInvoice.
Success returns the new invoice id { "data": { "id": <invoice id> } }; a failed create returns
respondBadRequestWithMsg with the action’s message.
Submit a full EOI — POST api/broker/launches/{publishedLaunch}/eois
Section titled “Submit a full EOI — POST api/broker/launches/{publishedLaunch}/eois”Multipart, async. SubmitFullEOIRequest (EOI/SubmitFullEOIRequest.php) is large; the core shape:
name, phone, email requiredpaid_amount, currency_id required · intpayment_status required · EOIPaymentStatusEnumspayment_method required · EOIPaymentMethodEnumsattachment_1 required · file · min:1 · max:5120 (KB)attachment_2 nullable · filecheque_number / cheque_attachment nullable (cheque path)instapay_* + instapay_attachment[] nullable (instapay path; each item {attachment file, referenceid})products[] present · arrayproducts[].quantity required · intproducts[].unittypes[] present · arrayproducts[].unittypes[].unit_type_id required · exists unit_types.idproducts[].unittypes[].project_id required · exists projects.idproducts[].unittypes[].preferences[] present · array (each {preference_source_id, rank})The controller stamps source_of_sale = BROKER, stores every uploaded file to the default disk under
eoi-documents, generates a unique EOI-<uuid> id, then dispatches SaveEOIToCrmJob and returns an
empty respondSuccess() immediately (EOIController.php:394-441). The CRM write happens in the queued job.
Generate the sales-offer PDF (async) — POST api/broker/units/{publishedUnitBroker}/sales-offer/{paymentPlanId}/generate
Section titled “Generate the sales-offer PDF (async) — POST api/broker/units/{publishedUnitBroker}/sales-offer/{paymentPlanId}/generate”throttle:sales-offer-generate (default 5/min). GenerateRequest (SalesOffer/GenerateRequest.php) takes
only an optional currency:
{ "currency": "<CurrencyEnums value, optional>" }The controller loads the unit relations, validates the sales-offer context for the resolved portal
(broker = portal 2), starts a SalesOfferPdfGeneration row, and dispatches GenerateSalesOfferPdfJob
(SalesOfferPdfController.php:26-56). It returns the tracking id:
{ "data": { "id": <generation id> }, "message": null }Poll status — GET …/sales-offer/{salesOfferPdfGeneration}/status
Section titled “Poll status — GET …/sales-offer/{salesOfferPdfGeneration}/status”Returns the GetGeneratedPdfStatus payload for the generation (status + a file URL once ready)
(:61-70). When the status is ready, follow the signed …/download URL — that route is
signed-verified and returns the raw PDF stream (no envelope), so the link works pasted into a fresh tab
and dies at its TTL.
Oman pre-registration — start, verify, and steps
Section titled “Oman pre-registration — start, verify, and steps”Start — POST …/oman-pre-registration-applications/start. StartPreRegistrationRequest:
{ "email": "broker@x.com", "broker_type": "ambassador | local_company | international_company" }broker_type must be one of OmanBrokerRegistrationTypeEnums. If an application is already in progress the
controller returns 422 with error application_already_in_progress; otherwise it sends the OTP
(PreRegistrationBrokerController.php:28-44). login takes only { "email": "..." } and re-sends an OTP if
the broker + application exist (else 404 application_not_found).
Verify OTP — POST …/otp/verify. VerifyOtpViaEmailRequest here additionally requires the email to
exists:oman_pre_registration_brokers,email. On success it deletes the OTP and mints the
pre-registration token:
{ "data": { "public_id": "...", "token": "N|xxxx", "token_type": "Bearer" }, "message": null }resend requires { "email": "...", "request_otp_type_for": "<OmanBrokerApplicationRequestOtpTypeEnums>" }
and only re-sends when the application status is valid for that OTP type.
Application steps (all authed with the pre-registration token; each returns the full
OmanPreRegistrationBrokerApplicationResource and advances the status machine):
PATCH ambassador-info—UpdateAmbassadorInfoRequest:name,phone,date_of_birth(date),nationality,country,city,address(must contain at least one letter and one digit),oman_sales_consultant_ids(array, min 1, eachexists:oman_sales_consultants,id), optionalbroker_card_number. Sets status toPENDING_DOCUMENTSand syncs the sales consultants (PreRegistrationApplicationController.php:26-55).PATCH company-info—UpdateCompanyInfoRequest; sets statusPENDING_COMPANY_DETAILS(:57-86).PATCH company-details—UpdateCompanyDetailsRequest; nullsother_issuing_authorityunlessissuing_authorityisOTHER; sets statusPENDING_DOCUMENTS(:88-112).PATCH finalize— only allowed when status isPENDING_DOCUMENTS, else 403pre_registration_wrong_application_status(:114-144).PATCH resubmit— only allowed fromMISSING_INFORMATION, else 403 (:146-176).PATCH submit-contract— only allowed fromCONTRACT_SENT, else 400 (:178-203).
Gotchas
Section titled “Gotchas”Other real quirks confirmed in code:
- Login is not in this file. Brokers authenticate at the shared
POST api/login; there is no broker login route underapi/broker. Only OTP-based onboarding (registration + Oman) lives here. - Two abilities, one file. The Oman pre-registration sub-tree uses
access-pre-registration-broker-api(broker.php:50-53); everything else usesaccess-broker-api. A token minted for one is rejected by the other — a common “why is my valid token 403” cause. GET documentsis a mock.UserController@getDocumentsreturns four hardcoded rows (Conflict Policy,Contract Agreement,Reservation Preview,Contract Preview) withdocument: "#"and no real integration (UserController.php:24-52).registeris Egypt-hardcoded.storeresolvesCountry::where('slug','egypt')regardless of the broker’s market (UserController.php:74). Same pattern for EOI submit and several EOI actions.- All PDF/file downloads bypass the envelope. Every
…/downloadand Oman documentdownloadroute useswithoutMiddleware([FormatApiResponse::class]), returning a raw stream — don’t parse them as{ data, message }. The sales-offer download additionally requires a validsignedsignature. - Two commented-out routes (
GET leads/sync,GET launches/sync,broker.php:28-29) are not registered — they are excluded from the 114 count. - Auth-guard driver mismatch (repo-wide).
config/auth.phpsetsguards.api.driver = passport, but the broker routes authenticate withauth:sanctum; Sanctum is the effective bearer guard. Passport is only used for thecrms-middlewareclient credentials.
