Broker lead API
Reference for the two broker lead endpoints. Request examples are real (captured from staging 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.robustastudio.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 leads-limit gate (inner data is a plain string — a reliable way to tell the
local gate from a CRM error):
{ "data": "You have reached the maximum number of leads for this destination.", "message": "Failure!" }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, from staging — values are placeholders)
Section titled “Response shape (real, from staging — 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.
