Property Map: - Replace ACF-based property display with MLS database queries - Use real lat/lng coordinates from MLS (100% coverage) - Create property-card-mls.php template for MLS property cards - Update AJAX handler to filter MLS properties MLS Plugin Enhancements: - Add 'wp mls run' smart sync command (auto-detects full/incremental/resume) - Add database index migrations for lat/lng and composite search indexes - Add comprehensive README.md documentation Documentation: - Update site README.md with sysadmin quick reference - Add FEATURES_PENDING_12_15.md tracking client feature requests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
MLS by HansonXyz
WordPress plugin for syncing MLS Grid API data (NorthStar MLS) into a local database with WP-CLI tools and a public API for themes and plugins.
Table of Contents
- Features
- Requirements
- Installation
- Configuration
- Running Sync
- WP-CLI Commands
- Cron Setup
- Public API
- Database Schema
- Media Handling
- Sync Strategy
- Error Recovery
- Troubleshooting
Features
- Syncs Active and Pending property listings from MLS Grid API
- Automatic incremental updates via replication
- On-demand image fetching and local caching
- Self-healing sync with automatic error recovery
- Rate limit compliance (MLS Grid limits enforced)
- Resume capability for interrupted syncs
- WP-CLI commands for all operations
- Public PHP API for theme/plugin integration
- Optimized database indexes for search queries
Requirements
- WordPress 5.0+
- PHP 7.4+
- MySQL 5.7+ or MariaDB 10.2+
- WP-CLI (for command-line operations)
- MLS Grid API access token
Installation
- Upload the
mls-by-hansonxyzfolder to/wp-content/plugins/ - Activate the plugin through WordPress admin
- Configure API credentials (see Configuration)
- Run initial sync:
wp mls run
Configuration
API Credentials
Add to your wp-config.php:
define('MLSGRID_API_URL', 'https://api.mlsgrid.com/v2');
define('MLSGRID_ACCESS_TOKEN', 'your-access-token-here');
WordPress Admin Settings
Navigate to Settings > MLS Settings to configure:
| Setting | Description | Default |
|---|---|---|
| Originating System | MLS identifier | northstar |
| Auto Sync | Enable WP-Cron sync | Disabled |
| Sync Interval | WP-Cron frequency | Hourly |
Running Sync
Smart Sync (Recommended)
The wp mls run command handles all scenarios automatically:
wp mls run # Smart sync with progress
wp mls run --quiet # Status messages only
wp mls run --verbose # Full API details
wp mls run --silent # For cron (exit code only)
Automatic behavior:
- If no data exists: runs full sync
- If data exists: runs incremental sync
- If previous sync failed: resumes from checkpoint
- If sync already running: safely aborts
Manual Sync Commands
For more control over sync operations:
# Full sync (Active/Pending properties only)
wp mls sync full
# Incremental sync (changes since last sync)
wp mls sync incremental
# Resume a specific failed sync
wp mls sync resume --id=<sync_id>
# Dry run (no changes)
wp mls sync full --dry-run --limit=100
Progress Indicators
During sync, progress characters indicate activity:
| Symbol | Meaning |
|---|---|
. |
Property created |
# |
Property updated |
x |
Property deleted |
! |
Error occurred |
| |
Page complete |
Use --verbose for detailed timestamped output.
WP-CLI Commands
Testing
wp mls test connection # Test API connectivity
wp mls test auth # Verify authentication
Status and Statistics
wp mls status # Full status overview
wp mls status rate-limits # Rate limit usage only
wp mls stats # Database statistics
Sync Operations
# Smart sync (recommended)
wp mls run [--quiet] [--verbose] [--silent]
# Manual sync
wp mls sync full [--dry-run] [--limit=N] [--verbose]
wp mls sync incremental [--dry-run] [--verbose]
wp mls sync resume --id=<sync_id>
Media Management
Images are fetched on-demand when properties are viewed. These commands manage the cache:
wp mls media status # Cache statistics
wp mls media fetch --listing=<key> # Pre-cache a listing's images
wp mls media fetch --listing=<key> --limit=10
wp mls media clear --listing=<key> # Clear cached images
Cache Management
wp mls cache clear --confirm # Delete ALL synced data
wp mls cache cleanup # Remove orphaned media files
wp mls cache missing # View failed media downloads
wp mls cache missing --clear # Clear the missing media log
Recovery
wp mls recovery list # Show resumable syncs
wp mls recovery auto # Auto-resume most recent failed sync
wp mls recovery cleanup # Mark stale syncs as failed
Cron Setup
Recommended Setup
Add to system crontab (crontab -e):
# Smart sync every 15 minutes (handles everything automatically)
*/15 * * * * cd /var/www/html && wp mls run --silent --allow-root >> /var/log/mls-sync.log 2>&1
This single entry handles:
- Initial full sync on first run
- Incremental updates on subsequent runs
- Automatic recovery from failures
- Safe concurrent execution (aborts if already running)
Alternative: Manual Control
# Incremental sync every 15 minutes
*/15 * * * * cd /var/www/html && wp mls sync incremental --allow-root >> /var/log/mls-sync.log 2>&1
# Full rebuild weekly (Sunday 3am)
0 3 * * 0 cd /var/www/html && wp mls cache clear --confirm --allow-root && wp mls sync full --allow-root >> /var/log/mls-sync.log 2>&1
Important Notes
- Use
--allow-rootwhen running as root - MLS Grid requires refresh at least every 12 hours per IDX rules
- Rate limits are handled automatically (plugin waits when approaching limits)
- No separate media cron needed - images are fetched on-demand
Public API
Available Functions
// Get properties with filters
$properties = mls_get_properties([
'status' => 'Active',
'city' => 'Albert Lea',
'min_price' => 100000,
'max_price' => 500000,
'min_beds' => 3,
'property_type' => 'Residential',
'limit' => 20,
'offset' => 0,
'orderby' => 'list_price',
'order' => 'DESC',
]);
// Get single property by listing key or MLS ID
$property = mls_get_property('NST123456');
// Get primary image (fetches on-demand if not cached)
$image_url = mls_get_property_image('NST123456');
$image_url = mls_get_property_image('NST123456', false); // Don't fetch, return null if uncached
// Get all images for a listing
$images = mls_get_property_images('NST123456'); // Fetch first 1 if uncached
$images = mls_get_property_images('NST123456', 10); // Fetch first 10 if uncached
$images = mls_get_property_images('NST123456', 0); // Don't fetch any
// Get media metadata (no fetching)
$media = mls_get_property_media('NST123456');
// Get distinct cities with listings
$cities = mls_get_cities(); // All cities
$cities = mls_get_cities('Active'); // Cities with active listings only
// Get property count
$count = mls_get_property_count(['status' => 'Active']);
// Check if data is available
if (mls_is_available()) {
// Show property search
}
// Get cache statistics
$stats = mls_get_cache_stats();
// Returns: ['total_media' => 50000, 'cached' => 1200, 'uncached' => 48800]
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status |
string | Active, Pending, Closed |
property_type |
string | Residential, Land, Commercial, etc. |
city |
string | City name |
county |
string | County name |
postal_code |
string | ZIP code |
min_price |
int | Minimum list price |
max_price |
int | Maximum list price |
min_beds |
int | Minimum bedrooms |
max_beds |
int | Maximum bedrooms |
min_baths |
int | Minimum bathrooms |
min_sqft |
int | Minimum living area |
max_sqft |
int | Maximum living area |
year_built_min |
int | Minimum year built |
year_built_max |
int | Maximum year built |
listing_key |
string | Specific listing key |
listing_id |
string | Specific MLS ID |
search |
string | Search address/remarks |
limit |
int | Results per page (default: 20) |
offset |
int | Pagination offset |
orderby |
string | Sort field |
order |
string | ASC or DESC |
include_media |
bool | Include media array |
fields |
array | Specific fields to return |
Property Object Fields
$property->listing_key // Unique identifier
$property->listing_id // MLS number
$property->standard_status // Active, Pending, Closed
$property->list_price // Current price
$property->original_list_price
$property->close_price
// Address
$property->street_number
$property->street_name
$property->street_suffix
$property->unit_number
$property->city
$property->state_or_province
$property->postal_code
$property->county
$property->latitude
$property->longitude
// Property details
$property->property_type
$property->property_sub_type
$property->bedrooms_total
$property->bathrooms_total
$property->bathrooms_full
$property->bathrooms_half
$property->living_area // Square feet
$property->lot_size_area
$property->lot_size_units
$property->year_built
$property->garage_spaces
// Description
$property->public_remarks
$property->directions
// Listing info
$property->list_agent_key
$property->list_agent_mls_id
$property->list_agent_name
$property->list_office_key
$property->list_office_mls_id
$property->list_office_name
// Dates and timestamps
$property->photos_count
$property->modification_timestamp
$property->photos_change_timestamp
$property->listing_contract_date
$property->close_date
$property->days_on_market
$property->created_at
$property->updated_at
Database Schema
Tables
All tables use the WordPress prefix (e.g., wp_mls_properties).
mls_properties
Main property listing data. Only Active and Pending properties are stored.
| Column | Type | Description |
|---|---|---|
| id | BIGINT | Auto-increment primary key |
| listing_key | VARCHAR(50) | Unique MLS Grid key |
| listing_id | VARCHAR(50) | MLS number |
| standard_status | VARCHAR(30) | Active, Pending |
| list_price | DECIMAL(15,2) | Current price |
| city | VARCHAR(100) | City name |
| latitude | DECIMAL(10,8) | GPS latitude |
| longitude | DECIMAL(11,8) | GPS longitude |
| ... | ... | See property fields above |
| raw_data | LONGTEXT | Full API response (JSON) |
| modification_timestamp | DATETIME | Last modified in MLS |
| created_at | DATETIME | Record creation |
| updated_at | DATETIME | Record update |
Indexes:
listing_key(UNIQUE)listing_idstandard_statuscityproperty_typelist_pricemodification_timestampbedrooms_totalcountyidx_latitude- for geo queriesidx_longitude- for geo queriesidx_status_city_price- composite for searchidx_status_type- composite for filtering
mls_media
Media metadata and cache status. Images are downloaded on-demand.
| Column | Type | Description |
|---|---|---|
| id | BIGINT | Auto-increment primary key |
| listing_key | VARCHAR(50) | Property reference |
| media_key | VARCHAR(100) | Unique media identifier |
| media_type | VARCHAR(30) | Photo, Document, etc. |
| media_order | INT | Display order |
| media_url | VARCHAR(1000) | Original MLS Grid URL |
| local_path | VARCHAR(500) | Cached file path |
| local_url | VARCHAR(500) | Cached file URL |
| downloaded_at | DATETIME | When cached |
mls_sync_state
Sync progress tracking for resume capability.
| Column | Type | Description |
|---|---|---|
| id | BIGINT | Sync operation ID |
| sync_type | VARCHAR(30) | full, incremental |
| status | VARCHAR(20) | pending, running, completed, failed |
| last_next_link | VARCHAR(2000) | Resume checkpoint |
| records_processed | INT | Total processed |
| records_created | INT | New records |
| records_updated | INT | Updated records |
| records_deleted | INT | Deleted records |
mls_rate_limits
API rate limit tracking.
mls_sync_log
Debug logging for sync operations.
mls_media_log
Media download audit trail.
Media Handling
On-Demand Fetching
Per MLS Grid rules, media URLs cannot be used directly on websites. Images must be downloaded and served from your own server.
How it works:
- Property sync stores media metadata (URLs, keys, order) but does NOT download images
- When
mls_get_property_image()is called, the image is fetched and cached locally - Subsequent requests serve from local cache
- Cache location:
wp-content/uploads/mls-listings/{prefix}/{listing_key}/
Benefits:
- No rate limit issues from bulk downloading
- Images cached only when needed
- Automatic re-fetch if cache cleared
- Works with MLS Grid's URL expiration
Pre-caching Images
To pre-cache images for specific listings:
wp mls media fetch --listing=NST123456 --limit=10
Cache Statistics
wp mls media status
Shows total media records, cached count, and uncached count.
Sync Strategy
Initial Import (Full Sync)
- Fetches ONLY Active and Pending properties
- Filter:
MlgCanView eq true AND (StandardStatus eq 'Active' OR StandardStatus eq 'Pending') - Uses
@odata.nextLinkfor pagination (NOT$skip) - Approximately 30,000 records for NorthStar MLS
- Takes 30-45 minutes on first run
Replication (Incremental Sync)
- Fetches ALL properties modified since last sync
- No filter on status (need to detect changes)
- For each record:
- If
MlgCanView = false: DELETE from local DB - If
StandardStatusnot Active/Pending: DELETE from local DB - Otherwise: INSERT or UPDATE
- If
Why This Approach?
- MLS Grid API limits
$skipto ~80,000 - bulk scanning fails - Only Active/Pending properties needed for display
- Replication is efficient - only fetches changes
- Proper deletion handling when properties sell
Error Recovery
Automatic Recovery
The plugin saves progress after each API page. If a sync fails:
- Progress is preserved in
mls_sync_statetable - Next
wp mls runautomatically resumes from checkpoint - Failed syncs older than 1 hour are marked for resume
Manual Recovery
# View resumable syncs
wp mls recovery list
# Auto-resume most recent
wp mls recovery auto
# Resume specific sync
wp mls sync resume --id=<sync_id>
# Mark stale syncs as failed
wp mls recovery cleanup
Troubleshooting
Connection Failed
wp mls test connection
wp mls test auth
Check:
- API token in wp-config.php
- Network connectivity
- MLS Grid API status
No Data After Sync
wp mls status
wp mls stats
Check:
- Rate limits (may need to wait)
- WordPress debug log for API errors
- Sync state for failures
Media Not Loading
wp mls media status
Check:
- Upload directory permissions
- Disk space
- MLS Grid media URL validity
Sync Taking Too Long
Initial sync of ~30K properties takes 30-45 minutes. Use --verbose to monitor progress.
Rate Limit Exceeded
The plugin automatically waits when approaching limits. If persistent:
- Reduce sync frequency
- Check for other API consumers
- Contact MLS Grid support
Clearing Data
To start fresh:
wp mls cache clear --confirm
wp mls run
Database Issues
If indexes are missing, trigger recreation:
wp eval "MLS_DB::create_tables();"
Rate Limits
MLS Grid enforces these limits:
| Limit | Value |
|---|---|
| Per second | 2 requests |
| Per hour | 7,200 requests |
| Per day | 40,000 requests |
| Data per hour | 4 GB |
The plugin automatically:
- Waits 500ms between requests
- Tracks hourly/daily usage
- Pauses when approaching limits
- Retries with exponential backoff on 429 errors
File Structure
mls-by-hansonxyz/
├── mls-by-hansonxyz.php # Main plugin file, public API
├── uninstall.php # Cleanup on uninstall
├── README.md # This file
├── admin/
│ └── class-mls-admin.php # WordPress admin interface
├── cli/
│ └── class-mls-cli.php # WP-CLI commands
├── includes/
│ ├── class-mls-activator.php # Plugin activation
│ ├── class-mls-api-client.php # MLS Grid API communication
│ ├── class-mls-db.php # Database operations
│ ├── class-mls-deactivator.php # Plugin deactivation
│ ├── class-mls-logger.php # Event logging
│ ├── class-mls-media-handler.php # On-demand image caching
│ ├── class-mls-options.php # Configuration management
│ ├── class-mls-query.php # Public query API
│ ├── class-mls-rate-limiter.php # Rate limit compliance
│ └── class-mls-sync-engine.php # Sync orchestration
└── docs/
├── API.md # MLS Grid API reference
├── CLAUDE.md # AI assistant context
└── USAGE.md # User documentation
Support
- Plugin logs: Settings > MLS Settings in WordPress admin
- Debug log:
wp-content/debug.log(if WP_DEBUG enabled) - MLS Grid API: support@mlsgrid.com
License
GPL-2.0+