Add MLS by HansonXyz plugin for MLS Grid API integration
Features: - Full sync of NorthStar MLS properties via MLS Grid API v2 - Incremental sync using ModificationTimestamp - Local media download and storage - Rate limit compliance (2 req/sec, 7200/hr, 40000/day) - Sync state tracking with resume capability - WP-CLI commands: test, sync, status, stats, cache - Admin settings page with manual sync triggers - Public API functions: mls_get_properties, mls_get_property, etc. Database tables: - mls_properties: Listing data with full field mapping - mls_media: Downloaded images - mls_sync_state: Sync progress tracking - mls_rate_limits: API usage tracking - mls_sync_log: Debug logging Documentation: - docs/CLAUDE.md: AI development guide - docs/API.md: MLS Grid API reference - docs/USAGE.md: User documentation Tested: Connection, auth, sync 10 records, media download verified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
# MLS Grid API Reference
|
||||
|
||||
Documentation for the MLS Grid API v2 used by this plugin.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://api.mlsgrid.com/v2
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Bearer token authentication via HTTP header:
|
||||
|
||||
```
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Required Header:**
|
||||
```
|
||||
Accept-Encoding: gzip
|
||||
```
|
||||
|
||||
The API requires gzip compression and will return error 400 without it.
|
||||
|
||||
## Rate Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Per Second | 2 requests |
|
||||
| Per Hour | 7,200 requests |
|
||||
| Per Day | 40,000 requests |
|
||||
| Data Per Hour | 4GB |
|
||||
|
||||
Exceeding limits returns HTTP 429 and temporarily suspends access.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Property
|
||||
|
||||
```
|
||||
GET /Property
|
||||
```
|
||||
|
||||
Main endpoint for listing data.
|
||||
|
||||
**Query Parameters:**
|
||||
- `$filter` - OData filter expression (required)
|
||||
- `$expand` - Include related resources: Media, Rooms, UnitTypes
|
||||
- `$top` - Records per page (max 5000, max 1000 with $expand)
|
||||
- `$select` - Specific fields to return
|
||||
- `$orderby` - Sort order
|
||||
|
||||
**Example:**
|
||||
```
|
||||
/Property?$filter=OriginatingSystemName eq 'northstar' and MlgCanView eq true&$expand=Media&$top=1000
|
||||
```
|
||||
|
||||
### Member
|
||||
|
||||
```
|
||||
GET /Member
|
||||
```
|
||||
|
||||
Agent/member records.
|
||||
|
||||
### Office
|
||||
|
||||
```
|
||||
GET /Office
|
||||
```
|
||||
|
||||
Brokerage office records.
|
||||
|
||||
### OpenHouse
|
||||
|
||||
```
|
||||
GET /OpenHouse
|
||||
```
|
||||
|
||||
Open house event records.
|
||||
|
||||
### Lookup
|
||||
|
||||
```
|
||||
GET /Lookup
|
||||
```
|
||||
|
||||
Field value definitions. Query no more than once per day.
|
||||
|
||||
## OData Filter Syntax
|
||||
|
||||
### Operators
|
||||
|
||||
| Operator | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| eq | Equals | `City eq 'Austin'` |
|
||||
| ne | Not equals | `Status ne 'Sold'` |
|
||||
| gt | Greater than | `ListPrice gt 200000` |
|
||||
| ge | Greater or equal | `BedroomsTotal ge 3` |
|
||||
| lt | Less than | `ListPrice lt 500000` |
|
||||
| le | Less or equal | `YearBuilt le 2020` |
|
||||
| and | Logical AND | `City eq 'Austin' and BedroomsTotal ge 3` |
|
||||
| or | Logical OR | Limited to 5 per query |
|
||||
| in | In list | `City in ('Austin', 'Dallas')` |
|
||||
|
||||
### Required Filters
|
||||
|
||||
Every Property request MUST include:
|
||||
|
||||
```
|
||||
OriginatingSystemName eq 'northstar'
|
||||
```
|
||||
|
||||
For initial import, add:
|
||||
```
|
||||
MlgCanView eq true
|
||||
```
|
||||
|
||||
### Timestamp Filters
|
||||
|
||||
For incremental sync:
|
||||
```
|
||||
ModificationTimestamp gt 2024-01-15T00:00:00.000Z
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
Responses include `@odata.nextLink` field containing URL for next page.
|
||||
|
||||
```json
|
||||
{
|
||||
"@odata.context": "...",
|
||||
"value": [...],
|
||||
"@odata.nextLink": "https://api.mlsgrid.com/v2/Property?$filter=...&$skip=1000"
|
||||
}
|
||||
```
|
||||
|
||||
Continue fetching until `@odata.nextLink` is absent.
|
||||
|
||||
## Property Fields
|
||||
|
||||
### Core Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| ListingKey | string | Unique identifier |
|
||||
| ListingId | string | MLS listing number |
|
||||
| StandardStatus | string | Active, Pending, Closed, etc. |
|
||||
| ListPrice | decimal | Listing price |
|
||||
| ClosePrice | decimal | Sold price |
|
||||
|
||||
### Address Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| StreetNumber | string | Street number |
|
||||
| StreetName | string | Street name |
|
||||
| StreetSuffix | string | St, Ave, Blvd, etc. |
|
||||
| UnitNumber | string | Unit/apt number |
|
||||
| City | string | City name |
|
||||
| StateOrProvince | string | State abbreviation |
|
||||
| PostalCode | string | ZIP code |
|
||||
| CountyOrParish | string | County name |
|
||||
| Latitude | decimal | GPS latitude |
|
||||
| Longitude | decimal | GPS longitude |
|
||||
|
||||
### Property Details
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| PropertyType | string | Residential, Land, Commercial, etc. |
|
||||
| PropertySubType | string | Single Family, Condo, etc. |
|
||||
| BedroomsTotal | integer | Total bedrooms |
|
||||
| BathroomsTotalInteger | integer | Total bathrooms |
|
||||
| BathroomsFull | integer | Full bathrooms |
|
||||
| BathroomsHalf | integer | Half bathrooms |
|
||||
| LivingArea | integer | Square feet |
|
||||
| LotSizeArea | decimal | Lot size |
|
||||
| LotSizeUnits | string | Acres, SqFt |
|
||||
| YearBuilt | integer | Year built |
|
||||
| GarageSpaces | integer | Garage spaces |
|
||||
|
||||
### Description Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| PublicRemarks | string | Property description |
|
||||
| Directions | string | Driving directions |
|
||||
|
||||
### Agent/Office Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| ListAgentKey | string | Listing agent ID |
|
||||
| ListAgentMlsId | string | Agent MLS ID |
|
||||
| ListOfficeKey | string | Listing office ID |
|
||||
| ListOfficeName | string | Office name |
|
||||
| ListOfficeMlsId | string | Office MLS ID |
|
||||
|
||||
### Timestamps
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| ModificationTimestamp | datetime | Last modified (use for sync) |
|
||||
| PhotosChangeTimestamp | datetime | Media last changed |
|
||||
| ListingContractDate | date | Listed date |
|
||||
| CloseDate | date | Sold date |
|
||||
| DaysOnMarket | integer | DOM count |
|
||||
|
||||
### MLS Grid Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| MlgCanView | boolean | OK to display (false = delete) |
|
||||
| MlgCanUse | array | Permitted use cases (IDX, VOW, etc.) |
|
||||
| OriginatingSystemName | string | Source MLS identifier |
|
||||
|
||||
## Media (via $expand)
|
||||
|
||||
When using `$expand=Media`, each property includes Media array:
|
||||
|
||||
```json
|
||||
{
|
||||
"Media": [
|
||||
{
|
||||
"MediaKey": "abc123",
|
||||
"MediaURL": "https://media.mlsgrid.com/...",
|
||||
"Order": 1,
|
||||
"ImageWidth": 1200,
|
||||
"ImageHeight": 800,
|
||||
"MediaModificationTimestamp": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** MediaURL is for downloading only. Store images locally.
|
||||
|
||||
## Sync Strategy
|
||||
|
||||
### Initial Import
|
||||
|
||||
1. Query with `MlgCanView eq true` to get viewable records
|
||||
2. Follow `@odata.nextLink` for pagination
|
||||
3. Store `ModificationTimestamp` from last record
|
||||
|
||||
### Incremental Sync
|
||||
|
||||
1. Query with `ModificationTimestamp gt {last_timestamp}`
|
||||
2. Do NOT filter by MlgCanView (need to see deletions)
|
||||
3. If `MlgCanView = false`, delete local record
|
||||
|
||||
### Media Sync
|
||||
|
||||
1. Check `PhotosChangeTimestamp` on each property
|
||||
2. If changed, replace all media for that listing
|
||||
3. Match by `MediaKey`, download via `MediaURL`
|
||||
4. Delete media where `MediaKey` no longer exists
|
||||
|
||||
### Error Recovery
|
||||
|
||||
Store `@odata.nextLink` after each page. On failure, resume from that URL.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Sequential requests only** - Do not parallelize API calls
|
||||
2. **Respect rate limits** - 2 req/sec max, pause if approaching limits
|
||||
3. **Use $expand wisely** - Reduces per-page limit from 5000 to 1000
|
||||
4. **Store raw JSON** - Keep original response for debugging
|
||||
5. **Query Lookup sparingly** - Once per day maximum
|
||||
6. **Don't hotlink media** - Download and serve from local storage
|
||||
|
||||
## Error Responses
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": 400,
|
||||
"message": "Error description",
|
||||
"target": "misc",
|
||||
"details": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 400 | Bad request (check filters, missing gzip) |
|
||||
| 401 | Unauthorized (invalid token) |
|
||||
| 429 | Rate limited (wait and retry) |
|
||||
| 500+ | Server error (retry with backoff) |
|
||||
|
||||
## Resources
|
||||
|
||||
- [MLS Grid Documentation](https://docs.mlsgrid.com/)
|
||||
- [API v2 Reference](https://docs.mlsgrid.com/api-documentation/api-version-2.0)
|
||||
- [Best Practices Guide](https://www.mlsgrid.com/resources)
|
||||
Reference in New Issue
Block a user