Skip to content

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 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 APIauth: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-registrationauth: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.

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)

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)

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

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

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

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

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

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

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)

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)

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().

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.

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 · string
contractual_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):

  1. If broker.can_submit_invoice is false, return 403 "Broker cannot submit invoice".
  2. Look up the AnalyticsBrokerSale by source_id = sale_source_id and lead_source_id. If absent, throw AppCustomException("This Sale not exists").
  3. Enforce ValidSubmitSecondSalesInvoice and ValidSubmitSalesInvoiceWhenContractualDate.
  4. Re-derive commission from the CRM via GetCommissionForSale (using country_id), then create the invoice through CreateLeadSalesInvoice.

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 required
paid_amount, currency_id required · int
payment_status required · EOIPaymentStatusEnums
payment_method required · EOIPaymentMethodEnums
attachment_1 required · file · min:1 · max:5120 (KB)
attachment_2 nullable · file
cheque_number / cheque_attachment nullable (cheque path)
instapay_* + instapay_attachment[] nullable (instapay path; each item {attachment file, referenceid})
products[] present · array
products[].quantity required · int
products[].unittypes[] present · array
products[].unittypes[].unit_type_id required · exists unit_types.id
products[].unittypes[].project_id required · exists projects.id
products[].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”

StartPOST …/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 OTPPOST …/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-infoUpdateAmbassadorInfoRequest: 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, each exists:oman_sales_consultants,id), optional broker_card_number. Sets status to PENDING_DOCUMENTS and syncs the sales consultants (PreRegistrationApplicationController.php:26-55).
  • PATCH company-infoUpdateCompanyInfoRequest; sets status PENDING_COMPANY_DETAILS (:57-86).
  • PATCH company-detailsUpdateCompanyDetailsRequest; nulls other_issuing_authority unless issuing_authority is OTHER; sets status PENDING_DOCUMENTS (:88-112).
  • PATCH finalize — only allowed when status is PENDING_DOCUMENTS, else 403 pre_registration_wrong_application_status (:114-144).
  • PATCH resubmit — only allowed from MISSING_INFORMATION, else 403 (:146-176).
  • PATCH submit-contract — only allowed from CONTRACT_SENT, else 400 (:178-203).

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 under api/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 uses access-broker-api. A token minted for one is rejected by the other — a common “why is my valid token 403” cause.
  • GET documents is a mock. UserController@getDocuments returns four hardcoded rows (Conflict Policy, Contract Agreement, Reservation Preview, Contract Preview) with document: "#" and no real integration (UserController.php:24-52).
  • register is Egypt-hardcoded. store resolves Country::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 …/download and Oman document download route uses withoutMiddleware([FormatApiResponse::class]), returning a raw stream — don’t parse them as { data, message }. The sales-offer download additionally requires a valid signed signature.
  • 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.php sets guards.api.driver = passport, but the broker routes authenticate with auth:sanctum; Sanctum is the effective bearer guard. Passport is only used for the crms-middleware client credentials.