· 3 min read · Zain Akram
RESO Web API integration in Laravel: a field guide from 100+ brokerages
How to pull MLS listings through the RESO Web API into a Laravel platform without drowning in dialects: OData queries, incremental sync, media, rate limits and the one normalization decision that keeps you sane.
- Laravel
- RESO Web API
- Real estate
- Data pipelines
If you build software for US real estate, you will meet the RESO Web API. It is the standard interface MLS boards expose for listings, and on paper it is one OData API. In practice every board has its own dialect: different field coverage, different media rules, different throttles, different ideas about what "active" means. This is how I integrate it in Laravel so the differences stay at the edge.
Know the shape of the API
RESO Web API is OData over HTTPS. The resources you will use most are Property, Media, Member (agents) and Office. You query with OData parameters: $filter, $select, $expand, $top, $orderby and, for paging, either $skip or the board's @odata.nextLink. Authentication is usually OAuth 2.0 client credentials or a long-lived bearer token issued by the board or its vendor.
Two facts drive the whole design:
- You cannot query live for every page view. Rate limits and latency make per-request lookups a bad idea. You replicate.
- `ModificationTimestamp` is your friend. Every record carries it, and it is the key to incremental sync.
Replicate, don't proxy
The platform keeps its own copy of listings. The initial load walks the full resource with $orderby=ModificationTimestamp and $top sized to what the board allows (often 200 to 1000). After that, a scheduled job asks for everything modified since the last watermark:
$filter=ModificationTimestamp gt 2026-10-01T00:00:00Z
&$orderby=ModificationTimestamp asc
&$top=500Store the watermark per board and per resource. Overlap it by a few minutes on each run; boards are not always strictly monotonic, and a missed record is worse than a repeated one if your writes are idempotent.
Deletes and status changes
Listings rarely disappear; they change status. Treat StandardStatus transitions as events, and run a periodic reconciliation that asks for the IDs of currently active listings and marks anything missing on your side as withdrawn. Some boards expose a deleted feed; most do not.
Media is a separate problem
Photos live in the Media resource, keyed by ResourceRecordKey. Pull them in a separate queued job, not inline with the listing sync, and store your own copies (S3 behind a CDN). Board image URLs expire, change and get throttled. Keep MediaModificationTimestamp so you only re-fetch what changed.
The normalization decision
Here is the choice that matters. Early versions of REConnect let feature code branch per board. It worked for three boards and collapsed at ten.
The fix was a single canonical listing model, with per-board adapters that do all the mapping at ingestion. Feature code never sees a raw RESO payload. Adding a board means writing one adapter and a test fixture, not touching search, pages or the agent tools. That decision is the reason the platform scaled to 100+ brokerages on one codebase.
Laravel specifics that help
- Queue everything. Sync jobs are long and must be resumable; use job batching and store progress in the watermark table, not in memory.
- Use a dedicated HTTP client per board with retry, backoff and a concurrency cap, and log the
$filteryou sent with every failure. - Validate adapters with recorded fixtures from each board. A board changing a field is a test failure, not a 2am surprise.
- Keep the canonical model lean and put board-specific extras in a JSON column. You will need them rarely, and they will not pollute your schema.
What to expect
A new board takes a day or two once the adapter pattern exists. Most of that time is reading the board's data dictionary and discovering which fields it actually populates. Budget for it, and never promise a client "we support RESO" without saying "and we will need a week per board for the first few".