Broker lead API
Reference for the two broker lead endpoints. Request examples are real (captured from a live environment and current code); response examples are real shapes with PII replaced by placeholders. See Broker submits a lead for how these fit together.
POST /api/broker/leads — submit a lead
Section titled “POST /api/broker/leads — submit a lead”| Auth | Authorization: Bearer <broker token> — Sanctum, ability access-broker-api |
| Rate limit | 60 req/min per user/IP |
| Controller | Api\Broker\LeadController@submitLead |
| Validated by | App\Http\Requests\Broker\SubmitLeadRequest |
Request body
Section titled “Request body”Every field, verbatim from SubmitLeadRequest::rules():
| Field | Rules | Notes |
|---|---|---|
customer_inquiry |
required, in enum | 1 investment · 2 unit-purchasing · 3 general. The form hardcodes 2. |
first_name |
required, string | |
last_name |
required, string | |
mobile_country_code |
required, string | e.g. "20" |
phone |
required, string | spaces stripped server-side |
destination_slug |
required, exists:destinations,slug |
swapped to destination_source_id before the CRM call |
phone_2 |
nullable, string | |
phone_3 |
nullable, string | |
email |
nullable, string | ⚠️ no email-format rule — any string is accepted |
budget |
nullable, numeric, min:0 |
|
interested_in_unit_id |
string, exists:units,id |
optional; mapped to the unit’s CRM source_id |
portal_page |
nullable, in enum | 1 unit · 2 destination · 3 compare · 4 talk-to-sales · 5 referral. Form hardcodes 1. |
portal_comments |
nullable, string | the form’s “Additional information” |
preferred_communication_method |
nullable, in enum | 1 virtual-meeting · 2 mobile · 3 email · 4 whatsapp |
preferred_communication_time |
nullable, in enum | 1 morning · 2 afternoon · 3 night |
Example request
Section titled “Example request”POST /api/broker/leads HTTP/1.1Host: api-orascom-uat.my.orascomdh.comAuthorization: Bearer <broker-sanctum-token>Content-Type: application/json{ "customer_inquiry": 2, "portal_page": 1, "destination_slug": "el-gouna", "first_name": "Jane", "last_name": "Doe", "mobile_country_code": "20", "phone": "1012345678", "phone_2": null, "email": "jane.doe@example.com", "budget": 5000000, "interested_in_unit_id": "1421", "portal_comments": "Interested in a 3-bed", "preferred_communication_method": 4, "preferred_communication_time": 2}Responses
Section titled “Responses”The backend double-wraps every response: an outer { data, message } plus an inner envelope.
200 — created (the inner message is empty; both source ids are returned):
{ "data": { "message": "", "data": { "lead_source_id": "CRM-LEAD-000123", "customer_source_id": "CRM-CONTACT-000123" }, "errors": null }, "message": "Success!"}422 — local refusal (inner data is a plain string — a reliable way to tell a local
refusal from a CRM error). There are two of these, and only the string distinguishes them:
{ "data": "You have reached the maximum number of leads for this destination.", "message": "Failure!" }{ "data": "Your account is not linked to a brokerage company, so leads cannot be created for this destination. Please contact your administrator.", "message": "Failure!" }The second is returned when the broker has no brokerage_company_id. The limit is counted per
brokerage company, so there is nothing to count against; it is refused rather than let through
uncounted. Only destinations that carry a leads_limit can produce either — every other
destination returns allowed before the check.
422 — validation error — standard Laravel shape, not re-wrapped (validation exceptions
bypass the success envelope): top-level { "message": "…", "errors": { "phone": ["…"] } }, no data.
400 — CRM error (the inner message carries the CRM/Logic-App reason).
| Status | Meaning | Where it comes from |
|---|---|---|
200 |
Lead created (one local audit row written on exact 200) | CRM returned 200 |
422 |
Local leads-limit gate (top-level data string) or validation (top-level errors) |
Local — never hit the CRM |
400 |
Logic App rejected the payload, or the middleware’s Azure-AD auth failed | Normalized — the middleware turns every non-200 CRM response into 400, with the upstream body in the inner message |
500 |
Middleware unreachable from the backend / a 300s timeout (ConnectionException), or the middleware’s “Unsupported Country” guard fired (see note) |
Infra, or a country not egypt/oman/montenegro (rare — country is derived from a validated destination) |
What the backend forwards to the CRM middleware
Section titled “What the backend forwards to the CRM middleware”SubmitNewLead forwards the whole validated body with three transforms, to
POST {CRMS_MIDDLEWARE_API_URL}/brokers/{brokerSourceId}/leads, header country: <slug>:
destination_slug→ removed, replaced bydestination_source_idinterested_in_unit_id→ the unit’s CRMsource_id(or"")phone→ spaces removed
Dynamics 365 field mapping (middleware)
Section titled “Dynamics 365 field mapping (middleware)”The middleware maps the unified fields to Dynamics lead entity fields
(SubmitNewLeadRequestMapper). One schema is used for all three countries on the write path:
| Dynamics field | ⟵ unified field | Dynamics field | ⟵ unified field | |
|---|---|---|---|---|
firstname |
first_name |
clientbudget |
budget |
|
lastname |
last_name |
customerinquiry |
customer_inquiry |
|
email |
email |
portalcomments |
portal_comments |
|
mobilecountrycode |
mobile_country_code |
portalpage |
portal_page |
|
fullmobilenumber |
phone |
preferedcommunicationmethod |
preferred_communication_method |
|
fullmobilenumber2 |
phone_2 |
preferedcommunicationtime |
preferred_communication_time |
|
fullmobilenumber3 |
phone_3 |
salesagentid |
broker_source_id (the broker) |
|
destinationid |
destination_source_id |
interestedin |
interested_in_unit_id |
|
country |
numeric id injected by the middleware |
Absent optional fields are sent to the Logic App as explicit null (they are not stripped).
GET /api/broker/leads — list the broker’s leads
Section titled “GET /api/broker/leads — list the broker’s leads”Returns the broker’s leads split into active and past deals. Optional ?search=<phone>.
| Auth | Bearer, ability access-broker-api |
| Read path | per-country direct Dynamics OData (not the Logic App) |
Response shape (real, captured from a live environment — values are placeholders)
Section titled “Response shape (real, captured from a live environment — values are placeholders)”{ "data": { "data": { "active_deals": [ { "lead_source_id": "00000000-0000-0000-0000-000000000000", "name": "<customer name>", "email": "<email>", "phone": "<phone>", "phone_2": null, "phone_3": null, "status_code": "100000000", "lead_status": "lead_submitted", "country_slug": "egypt", "country_id": 1, "destinationName": ["El-Gouna"], "interested_in_unit_details": null, "duplicate": false, "same_company": false, "sales": null, "lead_submitted_at": 1750428180, "broker": { "id": 49, "name": "<broker name>" } } ], "past_deals": [] } }, "message": "Success!"}lead_status is one of: lead_submitted, lead_verified, deal_closed, submitted_invoice,
invoice_approved, lead_reserved, past_deals (backend DealStatusEnums). lead_submitted_at
is a Unix epoch integer (or null), not a datetime string. The UI groups by broker when a
manager has multiple agents, and renders each as a card.
