Developer Documentation
Integration guide for displaying Source MLS badges on listing pages
Contents
Overview
Source MLS verifies that listing data displayed on real estate websites originates from an authorized MLS. Data Licensees, such as IDX Vendors, display a small badge on each listing detail page indicating this fact. The verification works through a simple SourceMLSURL that flows from the MLS to data Licensees via the RESO data feed.
How It Works
- MLS generates a SourceMLSURL for each listing in their RESO data feed. The URL contains a JWT token that encodes the listing and licensee information.
- Licensees receive the SourceMLSURL as a field in the RESO feed data for each listing.
- Licensees display a badge on their listing detail pages using the SourceMLSURL. The badge loads a small image and confirms the impression back to Source MLS.
SourceMLSURL Format
https://sourcemls.org/api/v1/{jwt_token}/badge
The {jwt_token} is a signed JWT containing RESO Data Dictionary claims that identify
the listing, licensee, and MLS organization. MLSs generate this token server-side using their
organization secret key.
For Licensees
As a data Licensee, you receive a simple and unique SourceMLSURL for each listing in the RESO data feed from
your MLS. To display the Source MLS badge on your listing detail pages, add the following
<img> tag.
Badge Implementation
Place this tag on each listing detail page, using the SourceMLSURL from that listing's feed data:
<img src="{SourceMLSURL}.png"
width="132"
height="60"
alt="Source MLS Verified"
onload="navigator.sendBeacon('{SourceMLSURL}')"
onerror="this.style.display='none'" />
How Each Attribute Works
| Attribute | Purpose |
|---|---|
src="{SourceMLSURL}.png" |
Loads the Source MLS badge image from the API. Appending .png to the SourceMLSURL requests the PNG format. This image is cached by browsers to improve performance. |
width="66" height="30" |
Sets the badge display size in pixels. |
onload="navigator.sendBeacon('{SourceMLSURL}')" |
After the badge image loads, sends a fire-and-forget POST to confirm the impression. The impression is confirmed whether the image loads the first time or from browser cache. |
onerror="this.style.display='none'" |
If the badge fails to load (network error, invalid token), hides the broken image element so it doesn't affect your page layout. |
https://sourcemls.org/api/v1/eyJhbGc.../badge,
your img tag src would be
https://sourcemls.org/api/v1/eyJhbGc.../badge.png and
the sendBeacon URL would be
https://sourcemls.org/api/v1/eyJhbGc.../badge.
style="background:white" to the <img> tag to ensure the badge remains readable.
Sample Badge Image
There is a sample badge image at https://sourcemls.org/sample-badge.png that you can use for testing image sizing and placement before your MLS starts providing real SourceMLSURLs in the feed.
For MLSs
As an MLS Organization, you generate a simple SourceMLSURL for each listing in your RESO data feed. The URL contains a JWT token signed with your organization's secret key. It is unique for each listing and licensee combination, allowing you to track the source and location of each badge impression.
You will want to work with your RESO Vendor or data feed provider to implement this. The Source MLS system will provide you with a secret key and your RESO Unique Organization Identifier once you sign up. Share those with your RESO Vendor or data feed provider and they can take it from there using the instructions below.
JWT Token Generation
Create a JWT token with the following RESO Data Dictionary claims, signed using your organization's secret key with the HS256 algorithm.
Required Claims
| Claim | Type | Description |
|---|---|---|
SourceSystemID |
String | Your MLS organization code. We use RESO's Unique Organization Identifier for this - see RESO UOI. |
LicenseeID |
String | Unique identifier for the licensee in your MLS system |
LicenseeName |
String | Display name for the licensee (e.g., company name) |
ListingID |
String | Unique listing identifier in your system |
StandardStatus |
String | Current listing status (Active, Pending, Sold, etc.). Use RESO Data Dictionary definition. |
ModificationTimestamp |
ISO 8601 | When the listing was last modified. Use RESO Data Dictionary definition. |
LicenseeID or ListingID doesn't exist in
Source MLS, it will be automatically created on first badge request. This enables zero-friction integration.
exp claim.
Assembling the SourceMLSURL
After generating the JWT token, construct the SourceMLSURL:
https://sourcemls.org/api/v1/{jwt_token}/badge
Include this URL as a field in the RESO feed data for each listing.
Sample Code
JavaScript (Node.js)
const jwt = require('jsonwebtoken');
// Generate JWT token with RESO claims
const payload = {
SourceSystemID: 'your-org-code',
LicenseeID: 'LIC-12345',
LicenseeName: 'ABC Realty Group',
ListingID: 'MLS-2026-12345',
StandardStatus: 'Active',
ModificationTimestamp: new Date().toISOString()
};
const token = jwt.sign(payload, process.env.MLS_SECRET_KEY, { algorithm: 'HS256' });
// Assemble the SourceMLSURL
const sourceMLSURL = `https://sourcemls.org/api/v1/${token}/badge`;
Ruby
require 'jwt'
# Generate JWT token with RESO claims
payload = {
SourceSystemID: 'your-org-code',
LicenseeID: 'LIC-12345',
LicenseeName: 'ABC Realty Group',
ListingID: 'MLS-2026-12345',
StandardStatus: 'Active',
ModificationTimestamp: Time.current.iso8601
}
token = JWT.encode(payload, ENV['MLS_SECRET_KEY'], 'HS256')
# Assemble the SourceMLSURL
source_mls_url = "https://sourcemls.org/api/v1/#{token}/badge"
Python
import jwt
import os
from datetime import datetime
# Generate JWT token with RESO claims
payload = {
'SourceSystemID': 'your-org-code',
'LicenseeID': 'LIC-12345',
'LicenseeName': 'ABC Realty Group',
'ListingID': 'MLS-2026-12345',
'StandardStatus': 'Active',
'ModificationTimestamp': datetime.utcnow().isoformat() + 'Z'
}
token = jwt.encode(payload, os.environ['MLS_SECRET_KEY'], algorithm='HS256')
# Assemble the SourceMLSURL
source_mls_url = f'https://sourcemls.org/api/v1/{token}/badge'
JWT Checker Tool
Use our online JWT checker to validate and decode your tokens during development.
Open JWT Checker ToolExporting Data
Vendors can programmatically export usage data from Source MLS using the Export API. This enables automated reporting workflows using the same MLS secret key used to generate SourceMLSURLs.
How It Works
- Request an export by sending a POST to
/api/v1/exportwith your MLS secret key, export type, time frame, and optional file format. - Receive an export ID immediately in the response. Since these files can be large and take time to generate, the export is processed asynchronously in the background.
- Poll for status by sending a GET to
/api/v1/export/:idwith your MLS secret key until the status is"completed". See details about a webhook alternative to polling below. - Download the file using the
download_urlprovided in the completed status response. The link expires after 72 hours.
POST /api/v1/export
Create a new export request. Returns 202 Accepted with an export ID for polling.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
mls_secret_key |
String | Yes | Your MLS organization's secret key (the same key used to sign JWT tokens for SourceMLSURLs) |
export_type |
String | Yes | Type of report to export (see valid types below) |
time_frame |
String | Yes | Time range for the data (see valid time frames below) |
file_format |
String | No | Output format: "csv" (default) or "json" |
webhook_url |
String | No | HTTPS URL to receive a notification when the export is ready |
Valid Export Types
| Value | Description |
|---|---|
analytics_overview |
Analytics Overview — summary of badge impressions and activity |
badge_requests_detail |
Badge Requests Detail — individual badge request records |
licensee_summary |
Licensee Summary — aggregated data by licensee |
Valid Time Frames
| Value | Description |
|---|---|
last_24_hours | Last 24 hours |
last_7_days | Last 7 days (from beginning of day) |
last_30_days | Last 30 days (from beginning of day) |
current_month | Current month to now |
last_month | Full previous month |
Request Example
curl -X POST https://sourcemls.org/api/v1/export \
-H "Content-Type: application/json" \
-d '{
"mls_secret_key": "your_secret_key_here",
"export_type": "badge_requests_detail",
"time_frame": "last_30_days",
"file_format": "json"
}'
Response (202 Accepted)
{
"export_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"export_type": "badge_requests_detail",
"file_format": "json",
"message": "Export queued. Poll GET /api/v1/export/:id for status, or await webhook delivery."
}
GET /api/v1/export/:id
Check the status of an export request. Pass your mls_secret_key as a query parameter.
The response fields vary based on the export status.
Request Example
curl "https://sourcemls.org/api/v1/export/EXPORT_ID?mls_secret_key=your_secret_key_here"
Response — Queued or In Progress
{
"export_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "in_progress",
"export_type": "badge_requests_detail",
"file_format": "json"
}
Response — Completed
{
"export_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"export_type": "badge_requests_detail",
"file_format": "json",
"download_url": "https://s3.amazonaws.com/...",
"expires_at": "2026-03-14T12:00:00Z",
"row_count": 1523,
"file_size_bytes": 245678
}
When the status is "completed", use the download_url to download the file.
The download link expires at the time shown in expires_at (72 hours after generation).
Response — Failed
{
"export_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"export_type": "badge_requests_detail",
"file_format": "json",
"error_message": "An error occurred during export generation"
}
Polling Example
A simple polling loop to wait for an export to complete:
#!/bin/bash
# Request an export
RESPONSE=$(curl -s -X POST https://sourcemls.org/api/v1/export \
-H "Content-Type: application/json" \
-d '{
"mls_secret_key": "your_secret_key_here",
"export_type": "badge_requests_detail",
"time_frame": "last_30_days"
}')
EXPORT_ID=$(echo $RESPONSE | jq -r '.export_id')
echo "Export ID: $EXPORT_ID"
# Poll until complete
while true; do
STATUS_RESPONSE=$(curl -s "https://sourcemls.org/api/v1/export/$EXPORT_ID?mls_secret_key=your_secret_key_here")
STATUS=$(echo $STATUS_RESPONSE | jq -r '.status')
if [ "$STATUS" = "completed" ]; then
DOWNLOAD_URL=$(echo $STATUS_RESPONSE | jq -r '.download_url')
echo "Download URL: $DOWNLOAD_URL"
curl -o export.csv "$DOWNLOAD_URL"
break
elif [ "$STATUS" = "failed" ]; then
echo "Export failed: $(echo $STATUS_RESPONSE | jq -r '.error_message')"
break
fi
echo "Status: $STATUS - waiting..."
sleep 5
done
Webhook Alternative
Instead of polling, you can provide a webhook_url in your export request.
When the export completes, Source MLS will send a POST request to your webhook URL with the export
details including the download_url.
curl -X POST https://sourcemls.org/api/v1/export \
-H "Content-Type: application/json" \
-d '{
"mls_secret_key": "your_secret_key_here",
"export_type": "analytics_overview",
"time_frame": "current_month",
"file_format": "csv",
"webhook_url": "https://your-server.com/webhooks/export-ready"
}'
Real-Time Stats
Get real-time badge request counts for your organization or individual listings. These lightweight endpoints return instantly and are designed for integration with vendor dashboards. They use the same MLS secret key as the Export API.
GET /api/v1/stats
Returns badge request counts for your entire MLS organization across four time windows.
Request Example
curl "https://sourcemls.org/api/v1/stats?mls_secret_key=your_secret_key_here"
Response (200 OK)
{
"mls_code": "M00000389",
"mls_name": "Doorify MLS",
"counts": {
"last_hour": 142,
"last_24_hours": 3850,
"last_7_days": 24300,
"all_time": 185420
}
}
GET /api/v1/stats/:listing_key
Returns badge request counts for a specific listing. The :listing_key is the listing's
unique identifier (the same ListingID value used in your JWT tokens).
Request Example
curl "https://sourcemls.org/api/v1/stats/OC25012345?mls_secret_key=your_secret_key_here"
Response (200 OK)
{
"mls_code": "M00000389",
"mls_name": "Doorify MLS",
"listing_key": "25012345",
"counts": {
"last_hour": 8,
"last_24_hours": 45,
"last_7_days": 312,
"all_time": 2840
}
}
Count Time Windows
| Field | Description |
|---|---|
last_hour | Badge requests in the trailing 60 minutes |
last_24_hours | Badge requests in the trailing 24 hours |
last_7_days | Badge requests in the trailing 7 days |
all_time | Total badge requests since the listing was first tracked |
Error Handling
All API endpoints return standard HTTP status codes and a consistent JSON error format.
HTTP Status Codes
| Status Code | Meaning |
|---|---|
200 |
Success |
202 |
Accepted - Export queued for processing |
204 |
No Content (sendBeacon POST success) |
400 |
Bad Request - Required JWT claims missing |
401 |
Unauthorized - Invalid credentials (JWT or MLS secret key) |
403 |
Forbidden - Organization mismatch |
404 |
Not Found - Resource doesn't exist |
422 |
Unprocessable Entity - Invalid parameter value |
500 |
Internal Server Error |
Error Response Format
All errors return a JSON body with a consistent structure:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": "Additional context (may be null)"
}
}
Error Codes
| Code | Status | Applies To | Description |
|---|---|---|---|
MISSING_SECRET_KEY |
401 | Export, Stats | No mls_secret_key parameter provided |
INVALID_SECRET_KEY |
401 | Export, Stats | Secret key is invalid or belongs to an inactive organization |
INVALID_EXPORT_TYPE |
422 | Export | Unrecognized export type (valid types listed in response) |
INVALID_TIME_FRAME |
422 | Export | Unrecognized time frame (valid frames listed in response) |
INVALID_FILE_FORMAT |
422 | Export | Unsupported file format (use csv or json) |
EXPORT_NOT_FOUND |
404 | Export | Export ID does not exist or belongs to another organization |
LISTING_NOT_FOUND |
404 | Stats | Listing key does not exist or does not belong to your organization |