MLS Website for Realtors: A WordPress Developer’s Implementation Guide

MLS Website for Realtors

MLS Website for Realtors: A WordPress Developer’s Implementation Guide

A client calls and says “I need a website with MLS listings.” Every word of that sentence contains a trap.

The MLS is not a public API. IDX is not a plugin. The RESO Web API is not the same as “MLS access.” And the live URL your client assumes will exist on day one is a prerequisite for the paperwork you haven’t started yet. In practice they’ve collapsed three separate systems into one phrase; you need to know which one they’re actually asking for before you quote.

An MLS website for realtors is a WordPress (or similarly CMS-based) real estate site that displays live, IDX-approved property listings pulled from a Multiple Listing Service through an authenticated API feed, typically the RESO Web API. The MLS is the source database, owned and operated by a regional board of REALTORS. IDX (Internet Data Exchange) is the policy and permission framework that allows member agents to publicly display each other’s listings.

The RESO Web API is the modern transport protocol, built on OData v4, that developer integrations authenticate against, usually through a vendor platform like Trestle, Bridge Interactive, or Spark API. Building one of these sites correctly means solving a multi-layer engineering problem that touches data pipelines, WordPress architecture, IDX compliance, hosting, and lead capture all at once.

This guide walks the full pipeline, every decision you’ll make, and introduces the shortcut stack (WPResidence plus MLSImport) that removes most of the custom work for standard deployments. By the end you’ll know where the failure modes live, what to quote, and how to avoid building the plumbing from scratch.

IDX, MLS, and the RESO Web API: What Every Developer Must Understand First

The most common scoping-call mistake is conflating three separate systems. Get this wrong and you underbudget by weeks.

MLS is the source database, regional and broker-owned. Roughly 580–635 MLSs operate in the US depending on counting methodology (RESO‘s certification map trends toward the higher figure). Every MLS is independently governed with its own rules.

IDX is Internet Data Exchange, the permission and display framework defined by NAR Policy Statement 7.58, not a technology. IDX approval is what lets a member agent publicly display another broker’s listings. When your client says “I need IDX,” they want permission, not a plugin.

RESO Web API is the modern retrieval protocol, RESTful and built on OData v4. The RESO Data Dictionary 2.0 was ratified in April 2024; NAR required all affiliated MLSs to certify by April 2025 (WAV Group). RETS, the predecessor protocol, is formally deprecated. Do not build new integrations against it.

As of 2025, ~93% of US MLSs are RESO-certified and over 75% expose a RESO Web API endpoint. Your actual entry point is almost never the MLS directly; it’s a vendor platform sitting in front of it: Trestle (CoreLogic, now Cotality), Bridge Interactive (Zillow Group), or Spark API (FBS). Vendors normalize authentication, expose RESO-compliant OData endpoints, and issue the OAuth credentials you use in production. You authenticate against Trestle. The MLS sits behind it.

Michael Wurzer, CEO of FBS and Vice Chair of RESO’s Board of Directors, both builds the API (through Spark) and governs the standard (through RESO). The line to internalize, quoted on the RESO site: “RESO does not provide MLS real estate data. RESO creates data standards.”

What your client calls “IDX access” is three separate approvals with three different timelines. Read this for more clarificaitons : What are IDX, MLS, RETS and RESO API

What Your Client Must Prepare Before You Write a Single Line of Code

The approval clock starts the moment the client submits paperwork, not when you start writing PHP. Get yourself on the email chain early, because the board will ask technical questions the client can’t answer and every missed response pushes the timeline another week.

Checklist for the kickoff:

  1. Active MLS membership, plus NAR membership for NAR-affiliated boards.
  2. Broker IDX participation confirmation. Individual agents inherit this, but verify it in writing.
  3. Completed MLS IDX application on the board-specific form. Some MLSs require the vendor to co-sign.
  4. API credentials request, often a separate form from IDX display approval.
  5. Website URL for the application. The site must exist, or provide a staging URL the board can load.
  6. Designated tech contact. Put yourself on this line, not the realtor.
  7. Agent branding assets: headshot, brokerage logo, contact info, bio. IDX compliance requires them.
  8. Domain and hosting provisioned. Start the API approval with the real production domain where you can.

Timeline: MLS approval can take several weeks, particularly for smaller boards. API setup and testing takes another one to three weeks after approval. Multiple markets mean independent approvals that stack rather than parallelize.

Common rejection reasons: no live URL, broker name displayed incorrectly, missing IDX disclaimer, or applying for data tiers the agent’s membership doesn’t cover. All preventable with a complete kickoff packet.

Not Every WordPress Theme Is Ready for MLS Data

Theme choice on an MLS site is a technical decision, not an aesthetic one. The first time a client shows up with a page-builder theme and asks you to add MLS listings, you’ll understand why theme selection belongs in the architecture conversation.

A generic WordPress theme has no property CPT, no search form wired to listing meta, no agent or office relationship model, no single-property template, no map component. Adding those from scratch is weeks of engineering before a single listing hits the database.

An MLS-ready theme ships with a property CPT, a search form that queries by price, beds, baths, city, type, and status, a half-map layout, a single-property template with gallery and inquiry form, and Agent and Office CPTs with property relationships.

The import layer needs a stable CPT to write into. If the property CPT is non-standard, every field has to be remapped, and a rename on the theme side breaks every imported listing. Page-builder themes have no property CPT at all; listing data has nowhere to land. You also should get informed about best MLS ready themes .

Authentication and API Credentials

All three major vendor platforms use OAuth 2.0. Bridge and Trestle use standard bearer tokens; Spark uses OpenID Connect on top of OAuth 2.0. Shape of the flow: client ID and secret go to the token endpoint, you get back an access token, and send it as an Authorization: Bearer <token> header on every RESO API request.

Credential storage is where most production incidents start. Rules: store client ID and client secret as wp-config.php constants or server-side environment variables (never plaintext in options); never expose credentials in frontend JavaScript (all API calls originate server-side); never commit .env or wp-config.php to version control; rotate tokens if exposure is suspected.

Minimal credential and memory-bump block:

// MLS API credentials, never in Git, never in the DB
define( 'MLS_CLIENT_ID',      getenv( 'MLS_CLIENT_ID' ) );
define( 'MLS_CLIENT_SECRET',  getenv( 'MLS_CLIENT_SECRET' ) );
define( 'MLS_TOKEN_ENDPOINT', 'https://api-prod.corelogic.com/trestle/oidc/connect/token' );

// Memory: bump for bulk import operations
define( 'WP_MEMORY_LIMIT',     '512M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

GitHub reported 39 million leaked secrets in 2024, a 67% year-over-year increase. Automated scanners hit public repos within five minutes of a push. A committed MLS secret is a credential rotation event, possibly a call with the compliance officer.

Two vendor gotchas. The Trestle endpoint is migrating from api-prod.corelogic.com to api.cotality.com (30-day notice before end of 2025). Hardcoded endpoint URLs will break. Bridge added per-minute burst rate limits on August 6, 2024: 1/15 of the hourly limit per minute, with Burst-RateLimit-Remaining and Burst-RateLimit-Reset headers on every response. Read them.

Data Retrieval Strategy

Before the pipeline details, name the architectural fork the client may not know exists. Iframe and widget-based IDX services (IDX Broker widget mode, Showcase IDX) are the shortcut that isn’t: a third-party JavaScript embed renders search results from the vendor’s infrastructure, listings live on the vendor’s domain, and Google never crawls them as your site’s content.

You hand styling, data control, and any SEO opportunity from listing pages to the vendor. Server-side import is the opposite trade: more engineering, real ownership of the CPT posts, theme-native templates, and the only architecture that can plausibly earn search traffic (limited as that traffic is, per the realistic SEO section later). This guide assumes import, because that’s the decision a developer makes for a client who wants the asset on their own domain.

Two retrieval modes: a one-time full import, and an ongoing incremental sync. The incremental sync is where OData v4 earns its keep.

For the incremental filter, use $filter=ModificationTimestamp gt <last_sync_datetime>. Store your last successful sync time in a WordPress option and pass it next run. The API returns only records that changed since. That’s the difference between pulling 12,000 listings every hour and pulling 40.

Pagination is $top and $skip. Page size: 200–500 records. Never assume you can fetch all listings in one request; even a 5,000-listing agent will hit response size limits.

Rate limits vary. Bridge: 1/15 of hourly per minute. Trestle: standard quotas with upgrade paths. Spark doesn’t publish numbers (contact api-support@sparkplatform.com). Check Burst-RateLimit-Remaining and back off. Exponential backoff on 429 and 503. Log partial completions so the next run resumes from the last successful position.

Set max_execution_time to at least 120 seconds. Run the initial import as a direct PHP script or WP-CLI command, not through the admin screen. A common first-build mistake is triggering the initial import from wp-admin, watching it time out at 30% through 12,000 listings, and discovering 3,600 duplicate CPT posts because the importer ran three partial times before anyone noticed.

On shared hosting with a 30–60 second execution ceiling (a common default on entry-level plans), the initial import for a large dataset does not complete. That shared-hosting failure mode is specific: the import times out, leaves thousands of incomplete records, and the client goes live with a fraction of their listings.

Benchmark: the MLSImport team reports 8,000 properties imported in “a few hours” on Cloudways managed hosting. That’s the practical reference point for a well-configured managed environment.

Field Mapping: From MLS Data to WordPress

The RESO Data Dictionary 2.0 defines 20-plus fields that matter for a standard listing display. You map each to a WordPress post meta key or a taxonomy term. The pattern you’ll maintain across every deployment:

// Pseudo field mapping: RESO DD 2.0 field -> WPResidence post meta key
$field_map = [
    'ListingKey'            => 'mls_listing_key',       // dedup key, store separately
    'ListingId'             => 'property_id',            // display MLS number
    'ListPrice'             => 'price',
    'UnparsedAddress'       => 'property_address',
    'City'                  => 'property_city',          // also map to city taxonomy
    'StateOrProvince'       => 'property_state',
    'PostalCode'            => 'property_zip',
    'Latitude'              => 'property_lat',
    'Longitude'             => 'property_lng',
    'BedroomsTotal'         => 'bedrooms',
    'BathroomsTotalInteger' => 'bathrooms',
    'PropertyType'          => 'property_type',          // also map to type taxonomy
    'StandardStatus'        => 'property_status',
    'PublicRemarks'         => 'post_content',
    'ListAgentMlsId'        => 'mls_agent_id',
    'ListOfficeMlsId'       => 'mls_office_id',
    'YearBuilt'             => 'year_built',
    'LivingArea'            => 'sqft',
    'LotSizeAcres'          => 'lot_size',
];
// Media lives in a separate RESO resource, fetch via ListingKey relationship

Two distinctions matter. ListingKey is globally unique within a board. ListingId is the agent-facing MLS number and is NOT unique across boards. Use ListingKey for deduplication. For multi-board installs, composite key is OriginatingSystemName plus ListingKey.

StandardStatus uses a fixed vocabulary: Active, Active Under Contract, Cancelled, Closed, Coming Soon, Delete, Expired, Hold, Incomplete, Pending, Withdrawn. Map it to a taxonomy on import so you can filter efficiently without expensive meta queries.

Media is a separate RESO resource. Fetch images with ResourceRecordKey eq <ListingKey>. The fields you care about are MediaURL, Order, and MediaType.

Even with DD 2.0 certification, some older MLSs still expose MLS-local names. Test against the actual endpoint, not the spec. Treat enumerations as volatile: Trestle’s DD 2.0 migration (Sept 30, 2024) changed enum values on nine fields including BuyerFinancing, Possession, LeaseTerm, MediaType. Integrations with hardcoded enum strings broke. Read enums from API metadata at runtime.

Local Storage Architecture in WordPress

Each listing becomes one CPT object. WPResidence uses estate_property. Match whatever your import layer writes. One CPT post per listing means each has its own URL, title, content, meta, and template.

Post meta in wp_postmeta performs well on small sites but begins to degrade on sites with several thousand listings once meta value scans become frequent. By 10,000 listings without an object cache or custom tables, meta_value BETWEEN queries on unindexed meta show up in the slow query log. Add indexes on the keys you query most, or use a custom table for high-cardinality numeric fields.

Taxonomies are your performance escape hatch. Map PropertyType, City, Area, and StandardStatus to taxonomy terms on import. A tax_query against indexed term-relationship tables outperforms a meta_query scan on the unindexed meta_value column — this is documented in Kinsta’s WordPress database optimization guide and is the widely recommended pattern for high-cardinality filter fields. Reserve meta_query for numeric range filters.

Agent and Office become their own CPTs, with relationships stored in post meta. This powers per-agent listing pages and correct broker attribution, which compliance requires anyway.

Media is the architectural fork. Images can go into the WordPress Media Library (wp_insert_attachment()) or stay as URL references to the MLS CDN. At 10,000 listings with 20 photos each, the local option means 200,000 image files in wp-uploads. The CDN-reference approach, which MLSImport uses by default, sidesteps the uploads problem entirely. It’s the decision that defines your hosting bill.

Unique Listing IDs and Deduplication

The create-versus-update logic is simple in concept and punishing when wrong. Store ListingKey as post meta on every imported CPT post. On each sync run, query for a post where meta_key = 'mls_listing_key' and meta_value = <ListingKey>. If found, update. If not, create.

Never use ListingId as the sole dedup key. It’s not unique across boards. Use it for display. Keep ListingKey for deduplication.

For multi-board installs, concatenate OriginatingSystemName and ListingKey into a composite string. This is the pattern that survives when a client adds a second market.

The worst failure mode is a feed reformat you didn’t see coming. When Trestle pushed the DD 2.0 migration in September 2024, some boards’ ListingKey values changed. Integrations that weren’t watching created new posts for every listing on the next sync: double the count, duplicate CPTs, broken canonical URLs, angry client. Monitor sync logs for unexpected creates. If a sync creates 6,000 posts on a site that usually creates five, your alert should fire before the client calls.

Sync Logic and Scheduled Jobs

Two-phase architecture: initial full import run once, then incremental sync on schedule. The incremental sync filters by ModificationTimestamp and runs hourly, exceeding the IDX compliance minimum of one refresh every 12 hours.

The single most common production failure on MLS sites is WP-Cron misfiring. WP-Cron is triggered by page visits: on a low-traffic staging site, a 2 AM sync can be missed entirely; on a high-traffic production site, every page load fires a DB check adding 50 to 150 ms of TTFB. Neither is acceptable.

Disable WP-Cron and replace it with system cron:

// In wp-config.php, disable the WP-Cron page-load trigger
define( 'DISABLE_WP_CRON', true );
# System crontab, run WP-Cron every 5 minutes
*/5 * * * * wget -q -O /dev/null https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1

This is the production-safe approach documented by SpinupWP and Kinsta. The first time WP-Cron misses a 2 AM sync on a zero-traffic staging site, and the client notices at 8 AM that listings are 14 hours stale, is the last time you’ll deploy without system cron.

One caveat: managed hosts (Kinsta, WP Engine, Cloudways) often configure server-side cron to trigger WP-Cron, which mitigates the unreliability at the platform level. Verify with your host. The problem is WP-Cron relying on page loads, not WP-Cron itself.

Process listings in batches of 50–200 per run. Store last_sync_timestamp and last_processed_page in options after each batch so the next run resumes cleanly. Log every run: start, fetched, created, updated, deleted, errors, end. If you can’t see what the last sync did, you can’t debug what it broke.

Handling Removed, Sold, and Expired Listings

StandardStatus is the source of truth for listing lifecycle. When a listing moves to Closed, Expired, Cancelled, or Withdrawn, you have three options, and which one you pick is constrained by MLS rules, not your preference.

  1. Delete: remove the post entirely. Simplest. Lowest compliance risk. Loses SEO equity (backlinks, indexed URL).
  2. Mark unavailable: set the post to draft or render a “no longer available” template. Preserves the URL. Requires compliance review, because if sold price display is restricted, the page must not display it in draft form either.
  3. Archive as Sold: change the taxonomy term to Sold, keep the page live with restricted fields removed. Only viable if the MLS explicitly allows sold data display.

This is not a pure engineering decision. Sold data display may be prohibited by your specific MLS even where other MLSs allow it. CRMLS and MFRMLS have different rules. Check the board’s IDX agreement, not just NAR’s floor. MLSImport defaults to deleting inactive listings, which is the safest compliance posture.

Recommendation for a first build: delete by default, document the decision in the handover. Revisit only if the client has a specific SEO case for sold-property content, and that content should become neighborhood market pages, not archived listings.

Media Import and Image Handling

Images live in a separate RESO Media resource, linked to Property by ResourceRecordKey = ListingKey. The rest is architectural choice.

Local import: download each image into the Media Library. Enables local CDN, WebP conversion, lazy loading. Downside: a 10,000-listing site with 20 photos each produces 200,000 image files. Initial import runs for hours.

MLS CDN references: store MediaURL directly as post meta, render it with a standard <img> tag pointing at the MLS CDN. MLSImport uses this model. From MLSImport’s FAQ: “No, we do not import thousands of images. We make sure images are served from your MLS so your server is not overcrowded.” The trade-off is that image delivery depends on the MLS CDN’s uptime and URL stability.

A 5,000-listing image import to the local Media Library on shared hosting will either time out or fill your disk quota. That’s the moment most developers switch to the CDN-reference approach.

The CDN dependency is real. Trestle published a notice in May 2021 about “Old MediaURL Format Being Disabled.” Images broke sitewide for integrations that hadn’t updated. Monitor image URL health. For local storage builds, set the Order=1 image as _thumbnail_id and defer wp_generate_attachment_metadata() to a background process.

Map and Geolocation Integration

Coordinates come from Latitude and Longitude in the Property resource. In the field these are among the least consistently populated values, especially at rural and smaller boards. Build a fallback geocoder: if lat/lng is missing, geocode UnparsedAddress with the Google Maps Geocoding API or Nominatim for non-production volumes. Cache the result.

Map provider choice: Google Maps JavaScript API (licensed per map load), or OpenStreetMap with Leaflet.js (free, good enough for real estate). WPResidence ships with Google Maps.

Marker clustering is not optional at scale. At 1,000-plus visible markers, individual marker rendering becomes a browser paint-cost problem. Use MarkerClusterer or Leaflet.markercluster. Don’t load all 5,000 markers on initial paint. Build an AJAX endpoint that returns only listings inside the current viewport, capped at 100 to 200 at a time, and paginate the rest.

Search and Filtering Performance

Standard WP_Query with meta_query for price, beds, and baths works adequately on small listing databases. Past several thousand listings, without indexes on wp_postmeta, it degrades badly.

tax_query is the recommended pattern for high-cardinality filter fields because taxonomy term relationships use indexed join tables rather than the unindexed meta_value column. Map PropertyType, City, Area, and StandardStatus to taxonomies at import time. Reserve meta_query for numeric ranges.

For five-filter combinations (price range + city + beds + type + status), direct $wpdb->get_results() with parameterized SQL often outperforms a stacked WP_Query. Use $wpdb->prepare(). Never concatenate user input.

Cache expensive queries with transients:

function get_listings_cached( $filters ) {
    $cache_key = 'listings_' . md5( serialize( $filters ) );
    $results   = get_transient( $cache_key );

    if ( false === $results ) {
        $results = new WP_Query( [
            'post_type'      => 'estate_property',
            'posts_per_page' => 20,
            'tax_query'      => [
                [ 'taxonomy' => 'property_city',   'terms' => $filters['city'],   'field' => 'slug' ],
                [ 'taxonomy' => 'property_type',   'terms' => $filters['type'],   'field' => 'slug' ],
                [ 'taxonomy' => 'property_status', 'terms' => 'active',           'field' => 'slug' ],
            ],
            'meta_query' => [
                [ 'key' => 'price',    'value' => [ $filters['min_price'], $filters['max_price'] ], 'type' => 'NUMERIC', 'compare' => 'BETWEEN' ],
                [ 'key' => 'bedrooms', 'value' => $filters['min_beds'], 'type' => 'NUMERIC', 'compare' => '>=' ],
            ],
        ] );
        set_transient( $cache_key, $results, 10 * MINUTE_IN_SECONDS );
    }
    return $results;
}

Cache TTL of 5 to 15 minutes balances freshness against database load. Invalidate on sync completion for the listings that changed. Standard WordPress transients automatically use the object cache backend (Redis, Memcached) when one’s available. At 10,000 listings, a meta_value BETWEEN query on price without a proper index shows up in the slow query log inside a week. Taxonomies are your friend.

Theme Compatibility: The Full Picture

An MLS-ready theme has to provide all of it at once: property CPT with stable meta key naming, a search form that queries both meta and taxonomies, single-property template with gallery, agent and office page templates, a map component, and a responsive mobile layout.

The import layer has to know the theme’s CPT slug and meta key names. A mismatch is a silent failure: listings import, display nothing on the front end. The post count goes up, search returns empty, and the client thinks the sync is broken when the theme template is the actual problem.

Popular MLS-ready themes in 2025–2026: WPResidence, Houzez, Real Homes, HomePress. MLSImport officially supports WPResidence, Houzez, Real Homes, WpEstate. Bespoke themes mean either matching MLSImport’s CPT structure or writing a custom mapper.

WPResidence: The MLS-Ready Theme for an MLS Website for Realtors

WPResidence is a real estate WordPress theme with 32,000-plus buyers, 1,600-plus five-star reviews, and 11 years in market, built specifically for MLS and IDX-connected real estate sites. Price is $79 one-time on ThemeForest. For a developer building an MLS website for realtors, this is the theme with the longest integration track record for the use case.

What it ships with that a generic theme does not:

  • estate_property CPT with stable meta keys, so the field mapping table from the previous section has a home.
  • Advanced search builder with filters for price, beds, baths, city, type, and status.
  • Half-map search layout (property cards beside the map).
  • Google Maps integration with markers and clustering.
  • Single property template with gallery, price, description, and inquiry form.
  • Agent and Office CPTs with bidirectional property relationships.
  • 50-plus ready-to-use demos, 450-plus theme options, full Elementor compatibility.

What WPResidence does not include: the MLS data connection itself. The theme is the presentation layer; it needs a data source, which is MLSImport (next section). The “MLS/IDX Ready Out of the Box” homepage claim is accurate in context: the theme is ready to receive and display MLS data but requires MLSImport (or a custom feed) to acquire it. If your client assumes the $79 purchase includes live listings, correct that expectation on the kickoff call.

Case study. makeaustinhome.info is Christina Catalano’s site, a realtor-broker in Austin, Texas, using WPResidence plus MLSImport against a live ACTRIS (Austin Board of Realtors) feed. Her framing maps directly to the scoping conversation you’ll have with your own clients: “When I became aware of the new standardized RESO interface for the MLS, I knew it was time to start searching for a better solution for my WordPress website to pull in listings […] I wanted a plugin that would easily integrate into my preferred WP Residence theme, and be easily customizable. MLS Import was my solution!”

Other verified production sites on this stack: caboproperties.com (Cabo MLS), statewideofhoughton.com (Michigan MLS), legacyhomerealtors.com (Louisville MLS), nirvanamiami.com (Miami MLS). Different markets, different vendor platforms, same stack.

MLSImport: The Import Layer

MLSImport is an idx plugin that handles the full data pipeline: RESO Web API authentication, field mapping, hourly sync scheduling, inactive listing deletion, and MLS CDN image routing. It removes the work described in the entire data pipeline section above from your scope on standard deployments. For any developer building a real estate website with MLS data on a standard single-board deployment, this is the tool that compresses the build from weeks to days.

What it handles so you don’t:

  • Authentication against Trestle, Bridge Interactive, Spark API, and other OData gateways (OAuth 2.0 and legacy RETS).
  • Field mapping through a configuration UI that maps RESO fields to WPResidence (or other supported theme) meta keys.
  • Sync scheduling: hourly automatic sync, initial full import, automatic deletion of inactive listings.
  • Media: images served from the MLS CDN, no local storage, no wp-uploads bloat.
  • Private field management to hide MLS-restricted fields without writing the filter yourself.

Supported MLSs: 800-plus markets across the US and Canada, including ACTRIS, Miami REALTORS, Stellar MLS, CRMLS, MLS PIN, Arizona Regional, Georgia MLS, NTREIS. Supported themes: WPResidence, Houzez, Real Homes, WpEstate. Pricing: $49/month after a 30-day free trial, annual at $42/month effective. Factor the subscription into the project budget.

Benchmark: the MLSImport team reports 8,000 properties imported in “a few hours” on Cloudways managed hosting. That’s the real-world reference point for a mid-size initial import on well-provisioned managed infrastructure.

Case study. adonait.com runs the Houzez theme plus MLSImport against Miami Association of REALTORS via Bridge Interactive. The typical pattern for large urban MLSs: data goes through Bridge, not directly. Nicolas Coudene of investatemiami.com, which runs on WPResidence plus MLSImport against Miami REALTORS, puts the timeline bluntly: “I found Wp Residence and MLS Import and got my project done in just a couple of days.” Days, not months. That’s the point of the shortcut stack.

The “No Coding Required” claim is true for standard single-board deployments on supported themes. Multi-board installs, non-standard field mappings, and custom search taxonomies still require developer intervention. Be honest with the client about where the plugin ends and your billable hours begin.

Compliance and Display Rules

IDX compliance is where an engineering-first mindset will get you sued. Rules are set by NAR at the floor and individual MLSs at the board level. Always check the MLS’s IDX rules. NAR’s floor is not the ceiling.

Broker attribution. NAR Policy Statement 7.58 requires the listing brokerage to be identified in a “reasonably prominent location” in a “readily visible color and typeface not smaller than the median used in the display of listing data.” Build that into your single-property template.

Required disclaimer. Every IDX page must include language along these lines: “IDX information is provided exclusively for consumers’ personal, non-commercial use, that it may not be used for any purpose other than to identify prospective properties consumers may be interested in purchasing, and that the data is deemed reliable but is not guaranteed accurate by the MLS.” Exact wording varies by MLS; use the board’s text. Global footer is standard.

Data refresh. IDX displays must refresh at least once every 12 hours; MLSImport’s hourly sync exceeds this. On a custom build, document the schedule and log every run. “We refresh hourly” is not a compliance posture; “here is the log showing hourly refresh for 90 days” is.

Sold data. MLSs must allow sold data display from January 1, 2012 per NAR policy, but sale price display may be prohibited by individual boards. CRMLS and MFRMLS differ.

Field visibility. Some fields (showing instructions, lockbox info, seller concessions) are restricted to MLS members and must not appear publicly. MLSImport’s “Select what fields are private” handles this. Custom builds apply the same filtering server-side. Client-side hiding is not filtering.

Virtual tours. Trestle exposes branded and unbranded variants. Some MLSs require unbranded URLs in certain contexts (Trestle Content Patch #145). Pick the correct field for each display location.

Error Handling and Logging

The most common silent production failure is a 401 at 3 AM when an OAuth token expires with no refresh logic. The sync stops. Listings go stale. The client notices in two days. Build token refresh first.

Response code handling:

  • 401 Unauthorized: token expired. Refresh and retry once. If the refresh also fails, log and alert.
  • 429 Too Many Requests: rate limited. Back off for the duration in Retry-After or Burst-RateLimit-Reset. Do not retry immediately.
  • 503 Service Unavailable: vendor outage. Log, alert, retry in 30 minutes. Never chain rapid retries. You will not make the outage end faster.
  • PHP timeout mid-batch: log the last successfully processed ListingKey so the next run resumes from there.

Partial import recovery depends on a sync cursor (last timestamp plus last ListingKey) in options. Image URL failures on the CDN model produce 404s in the browser, not PHP errors. Run a periodic cron health check that pings a sample of MediaURL values.

Surface sync status to the client with an admin notice showing the last run result: success or failure, count delta, error codes. Prefer structured JSON logs over plain text. You will grep them later.

Hosting Requirements

Benjamin Levy at MLSImport published detailed hosting requirements in February 2026. His numbers are the ones to quote.

Setting Minimum Recommended
memory_limit 256M 512M for imports over 3,000 listings
max_execution_time 60s 120 to 300s for sync operations
max_input_vars 1000 5000-plus for plugin setup screens
PHP version 7.4 8.x with OPcache enabled
Database MySQL / MariaDB SSD-backed, properly indexed
Cron WP-Cron (dev only) System cron (production always)

Scale tiers from MLSImport’s hosting docs:

  • Up to ~3,000 listings: decent shared hosting with SSD, 256 to 384 MB PHP memory.
  • 3,000 to 7,000: managed WordPress hosting (Cloudways, Kinsta, WP Engine). Shared hosting starts to strain.
  • 7,000 to 20,000: VPS with 2 to 4 vCPUs and 4 to 8 GB RAM. This is the transition point MLSImport officially calls out.
  • 20,000 to 50,000: cloud VPS, 2 to 4 GB RAM minimum, 2-plus vCPUs, SSD critical.
  • 50,000-plus: cloud VPS with 2 to 4-plus GB RAM and routine database maintenance.

Because MLSImport serves images from the MLS CDN, disk sizing is about structured data (MySQL), not media files. 10,000 listings means several hundred thousand rows across wp_posts and wp_postmeta. SSD I/O matters more than raw disk.

Redis, via the redis-cache plugin, reduces repeat MySQL reads by over 70% for cached objects (per MotoCoders) and should be treated as required infrastructure for any MLS site over ~3,000 listings. Target a hit rate above 90–95%.

Full-page cache must be invalidated when the sync completes. Stale sold listings appearing as active is a compliance risk, not a UX annoyance. Always test the initial import on staging. The shared hosting failure mode is specific: the initial import times out, leaves thousands of incomplete records, and the client goes live with half their listings.

Security Considerations

Credentials first. Store client ID and client secret as server-side environment variables or wp-config.php constants. Never the options table, never theme or plugin files, never Git. As noted in the credentials section above, GitHub logged 39 million leaked secrets in 2024 — scanners find credentials within five minutes of a push.

Sanitize every field from the MLS API response before insertion. sanitize_text_field() for strings, absint() for integers, esc_url_raw() for URLs. The MLS API is external data. Never trust external data.

Escape on output: esc_html(), esc_url(), wp_kses_post(). No raw echo of API response content.

AJAX endpoints serving listing data (map markers, search results) must verify a nonce and check capabilities. Unauthenticated AJAX is fine for public search but must never expose private fields (contact info, showing instructions, seller data) even if they’re in post meta.

Implement field visibility server-side. A listing page must not expose MLS-restricted fields even if they’re stored in post meta. CSS or JavaScript hiding is not hiding; scrapers will find them in seconds.

Lead Capture and CRM Flow

Every listing page is a lead entry point. An MLS website has three standard capture surfaces: the contact form on the single property page (“request a showing,” “ask about this listing”), saved search registration, and property favorites. Each generates a lead tied to a specific listing. The critical detail is that the lead record must store the listing reference (ListingKey or CPT post ID) alongside the usual contact fields. Without the listing context, agent routing, property matching, and follow-up context all fail silently.

The native CRM for WPResidence is WPEstate CRM, documented in an April 2026 article by Cris Bean. It runs entirely inside WordPress on eight custom tables, no external SaaS subscription. That architectural choice is what matters most for client budget conversations.

What WPEstate CRM handles:

  • Direct hooks into WPResidence’s contact forms. Form submissions auto-populate buyer preferences (property type, price range, location) from the listing context.
  • A tracking cookie that identifies returning visitors and updates existing contact records instead of creating duplicates.
  • “Find Matching Properties” queries live estate_property posts using the same taxonomy and meta structure WPResidence uses. One-click email of matches from inside the CRM.
  • Kanban pipeline with configurable stages and stage_changed_at timestamps.
  • 27 pre-built automation rules across five categories (new lead handling, stale lead recovery, deal pipeline, task management, property matching). Event-based or time-based via WP-Cron.
  • Six notification event types with 20 placeholder tokens, plus SMS via Twilio.
  • Optional two-way sync with the HubSpot CRM v3 API.
  • Agent routing: leads from a listing auto-assign to that listing’s agent; agents see only their pipeline.
  • A nine-page frontend dashboard, so agents work without wp-admin access.

Your job as the developer is to wire it up, not use it. Configure the contact form to CRM handoff, verify the listing ID is passed on every form submission, and configure automation timing against the WP-Cron schedule. The common oversight is failing to pass the listing ID: the CRM creates the lead but has no property context, so matching and routing fail silently.

For custom builds without WPResidence, capture listing ID plus buyer preferences on every lead submission, integrate with a CRM API, and build routing rules. Specifics differ. The data model doesn’t.

The Honest SEO Picture for an MLS Website for Realtors

Every developer who takes on an MLS project eventually has to reset the client’s expectation that 10,000 listing pages will send 10,000 streams of organic traffic. Set this expectation at kickoff.

Individual listing pages are near-identical across thousands of IDX sites. Every agent in a given MLS displays the same address, price, beds, baths, and description. Google recognizes the pattern. No ranking penalty per se (Google says it “can handle multiple URLs to the same content”), but canonicalization tends to favor the portals (Zillow, Realtor.com, Redfin) or the listing broker’s site. Agent IDX pages rarely win that contest.

Listing lifespans make it worse. Active listings go to Closed or Withdrawn in days to weeks. Indexed ranking is transient. Per-listing SEO has near-zero compounding return.

What to tell the client: listing pages drive traffic when the buyer already knows the listing (direct nav, email, social share). They convert visitors to leads and build trust. They don’t drive organic search discovery in aggregate.

What actually ranks on agent sites: neighborhood and city pages with unique content. School info, commute data, market stats, lifestyle. Stable (no expiration), non-duplicate (you write them), targeting long-tail queries (“3 bedroom homes under $400k in [neighborhood]”) that portals handle poorly. Site architecture: homepage to city pages to neighborhood pages to listing search to individual listings. Invest content budget in the city and neighborhood pages.

Canonical strategy per Google Search Central: self-canonical tags on all listing pages (standard Yoast/Rank Math). noindex on thin search results and archive pages with query parameters. Do NOT noindex individual property detail pages. Let them be crawled and indexed; just don’t expect them to rank.

The trust context: 43% of buyers first found the home they purchased online in 2024, per NAR’s 2024 Profile of Home Buyers and Sellers. The site still matters. It matters for conversion, not primarily for listing-page rankings.
Read about it on : real estate website SEO guide

With the full pipeline mapped and the SEO reality established, the remaining decision is which path you build it on.

WPResidence plus MLSImport is the right choice when the client runs a standard deployment (single MLS board, typical property types), the project must ship in days not months, and you want to focus on customization rather than MLS plumbing. The shortcut stack removes authentication, field mapping, sync, media, and deduplication from scope. Cost: $49/month for MLSImport plus $79 one-time. Most single-market realtor clients fall here.

Custom RESO integration is the right choice when the client has non-standard schema needs (multiple boards, custom fields, complex multi-market search), needs deep integration with a proprietary CRM, or has existing WordPress architecture that WPResidence would replace. Cost: weeks to months of engineering, ongoing maintenance, risk exposure on every vendor migration (the Trestle URL change being the current example).

Neither answer is wrong. The shortcut constrains you to WPResidence’s schema. Custom gives control and costs real time. Make the call consciously in the scoping conversation, not after the first sync fails.

An MLS website for realtors built on WPResidence and MLSImport can go from zero to live listings in a weekend. A custom RESO integration genuinely cannot promise that. Before quoting either path, run a test import on staging with a live MLS credential. The most important decision in any MLS website development project is made on the kickoff call, not on the commit that fixes the first bug.

Read next