Download OpenAPI specification:
Integrate CMAsnap into your product or workflow.
Every API call acts as the user who authorized the token: it sees only that user's reports and that user's MLS coverage, and creating reports requires an active CMAsnap subscription.
Main entry point for creating CMA reports with authenticated links.
Note: When using mlsId or address parameters, the system will first attempt to find an existing report for that property. If found, it will load the existing report; otherwise, it will create a new one.
Add ?autoLogin=tangilla to attempt automatic Tangilla authentication:
https://app.cmasnap.com/new?mlsId=PROP123&autoLogin=tangilla
This will redirect users to Auth0 with the Tangilla connection pre-selected, streamlining the login process for Tangilla users.
Add ?autoLogin=realoms to attempt automatic Realoms SAML SSO:
https://app.cmasnap.com/new?mlsId=PROP123&autoLogin=realoms
This redirects users to Auth0, which initiates a SAML 2.0 authentication flow against Realoms (realoms.com) as the Identity Provider.
Add ?autoLogin=maris to attempt automatic MARIS authentication:
https://app.cmasnap.com/new?mlsId=PROP123&autoLogin=maris
| mlsId | string Example: mlsId=PROP123456 MLS listing ID - searches for existing report or creates new one for this property |
| address | string Example: address=123%20Main%20St%2C%20Austin%2C%20TX URL-encoded property address - searches for existing report or creates new one at this address |
| embed | boolean Default: false Enable embed mode for iframe usage |
| autoLogin | string Enum: "tangilla" "maris" "realoms" Attempt auto-login with specified provider |
Embed CMAsnap functionality directly into your website using authenticated iframes.
Note: The actual URL is /new?embed=true but this documentation is for the embedding functionality.
<iframe
id="cmasnap-iframe"
src="https://app.cmasnap.com/new?embed=true"
scrolling="no"
style="width: 1px; min-width: 100%; border: 0; overflow: hidden; height: 500px">
</iframe>
<iframe
id="cmasnap-iframe"
src="https://app.cmasnap.com/new?embed=true&mlsId=PROP123"
scrolling="no"
style="width: 1px; min-width: 100%; border: 0; overflow: hidden; height: 500px">
</iframe>
<iframe
id="cmasnap-iframe"
src="https://app.cmasnap.com/new?embed=true&address=123%20Main%20St"
scrolling="no"
style="width: 1px; min-width: 100%; border: 0; overflow: hidden; height: 500px">
</iframe>
<script src="https://app.cmasnap.com/static/js/embed.js"></script>
<script>
const cmasnapInt = setInterval(() => {
if (typeof CMAsnap !== 'undefined') {
new CMAsnap('#cmasnap-iframe');
clearInterval(cmasnapInt);
}
}, 1000);
</script>
Users will need to authenticate within the iframe on their first visit. Authentication persists across sessions.
| embed required | boolean Value: true Must be set to true for embed mode |
| mlsId | string MLS listing ID to load |
| address | string Property address (URL encoded) |
| autoLogin | string Enum: "tangilla" "maris" "realoms" Pre-select authentication provider |
Create, list, and read CMA reports.
Report generation is asynchronous: POST /api/reports returns 202 Accepted immediately
with the new report's id, and the comparable search, adjustments, and valuation run in the
background. Poll GET /api/reports/{reportId}/status
until status is ready (typically a few seconds to a minute) before reading the report.
Send Accept: application/json on every request so authentication failures come back as a
401 instead of a redirect to the login page.
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "metadata": {
- "Address": "123 Main St, Austin, TX 78701",
- "MLS #": "string",
- "Coordinates": [
- 0
], - "reportType": "Residential",
- "status": "queued"
}, - "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z"
}
]Queues a new CMA report and returns 202 Accepted with the report id. Poll
/api/reports/{reportId}/status until it is
ready.
Identify the subject property with one of:
address — a full street address, including city and state (ZIP recommended). CMAsnap
geocodes it and matches it to a listing in the user's MLS coverage. When the property has
several listings (e.g. a past sale and a current rental), the newest listing matching
reportType is used.listingId — the MLS number of a listing in one of the user's authorized MLS
datasets. Use this when you already know the listing; it skips address matching.If both are sent, listingId wins. Comparables are selected automatically.
curl -X POST "https://app.cmasnap.com/api/reports" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"address": "123 Main St, Austin, TX 78701"}'
Requires an active CMAsnap subscription.
| listingId | string Example: listingId=ABC123 MLS listing number of the subject property. Required unless |
| address | string Example: address=123%20Main%20St%2C%20Austin%2C%20TX%2078701 Full street address of the subject property (URL-encoded). May be sent in the body instead. |
| address | string Full street address of the subject property. Required unless |
| listingId | string MLS listing number of the subject property. Required unless |
| reportType | string Enum: "Residential" "Residential Lease" Defaults to |
| compSelectionDistance | number Optional search radius for comparables, in miles |
{- "address": "123 Main St, Austin, TX 78701",
- "listingId": "string",
- "reportType": "Residential",
- "compSelectionDistance": 0
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "metadata": {
- "Address": "123 Main St, Austin, TX 78701",
- "MLS #": "string",
- "Coordinates": [
- 0
], - "reportType": "Residential",
- "status": "queued"
}, - "state": {
- "status": "queued",
- "properties": [ ]
}
}Returns the full report: the subject property (targetProperty), the selected comparables
with adjustments (compProperties), the report settings, and the valuation. The nearby
market listings are paginated separately via
/api/reports/{reportId}/properties.
Property records use RESO-style field names (Address, List Price, Sold Price,
Living Area, Bedrooms, Baths Total, MLS #, Status, Coordinates, …). The set of
fields available depends on the MLS.
| reportId required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "metadata": {
- "Address": "123 Main St, Austin, TX 78701",
- "MLS #": "string",
- "Coordinates": [
- 0
], - "reportType": "Residential",
- "status": "queued"
}, - "state": {
- "targetProperty": {
- "ListingKey": "string",
- "MLS #": "string",
- "Address": "string",
- "Status": "string",
- "List Price": 0,
- "Sold Price": 0,
- "Living Area": 0,
- "Bedrooms": 0,
- "Baths Total": 0,
- "Coordinates": [
- 0
]
}, - "compProperties": [
- {
- "ListingKey": "string",
- "MLS #": "string",
- "Address": "string",
- "Status": "string",
- "List Price": 0,
- "Sold Price": 0,
- "Living Area": 0,
- "Bedrooms": 0,
- "Baths Total": 0,
- "Coordinates": [
- 0
]
}
]
}
}Paginated list of the active, pending, and sold listings near the subject property.
| reportId required | string <uuid> |
| offset | integer Default: 0 |
| limit | integer Default: 25 |
{- "properties": [
- {
- "ListingKey": "string",
- "MLS #": "string",
- "Address": "string",
- "Status": "string",
- "List Price": 0,
- "Sold Price": 0,
- "Living Area": 0,
- "Bedrooms": 0,
- "Baths Total": 0,
- "Coordinates": [
- 0
]
}
], - "total": 0,
- "offset": 0,
- "limit": 0,
- "hasMore": true
}Report payloads carry only the first photo per property; fetch the full gallery here.
| reportId required | string <uuid> |
| listingKey required | string The property's |
{- "listingKey": "string",
- "media": [
- { }
]
}Webhooks send a signed HTTPS POST to your server when something happens in a user's
CMAsnap account — a contact is added or changed, a contact opens a shared report, or a report
finishes generating.
In CMAsnap, go to Integrations → Webhooks and create a webhook with:
contact.created, contact.updated, contact.activity, or
report.generated. Create one webhook per event you want.https:// endpoint on your server.contact.activity types.The signing secret (whsec_…) is shown once, when the webhook is created. Store it
securely; you can rotate it from the webhook's page at any time.
Webhooks require an active CMAsnap subscription.
Every delivery is a JSON POST with this envelope:
{
"event": "report.generated",
"createdAt": "2026-09-30T16:40:25.000Z",
"data": { }
}
| Header | Value |
|---|---|
Content-Type |
application/json |
X-CMAsnap-Event |
The event name, e.g. contact.created |
X-CMAsnap-Webhook-Id |
The ID of the webhook that sent the delivery |
X-CMAsnap-Signature |
sha256=<hex> — see below |
X-CMAsnap-Signature is an HMAC-SHA256 of the raw request body using your signing
secret, formatted as sha256=<hex>. Compute it over the exact bytes you received, before
parsing the JSON.
For 24 hours after you rotate a secret, the header carries two comma-separated signatures (new secret first, previous secret second) so you can roll the secret on your side without dropping deliveries. Accept the request if any of them matches.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhooks/cmasnap', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.CMASNAP_WEBHOOK_SECRET)
.update(req.body) // raw Buffer
.digest('hex');
const signatures = (req.get('X-CMAsnap-Signature') || '').split(',');
const valid = signatures.some((sig) =>
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
if (!valid) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body);
// ...handle the event
res.sendStatus(200);
});
Respond with any 2xx status within 10 seconds. Anything else — a non-2xx status, a timeout, or a connection error — is retried with exponential backoff (5 attempts starting at 2 seconds by default, configurable per webhook). Do slow work asynchronously after responding.
Deliveries can repeat (a retry after a timeout, a manual redelivery), so make your handler
idempotent. Events are not debounced: a contact opening a report five times sends five
contact.activity events.
Each webhook's page in CMAsnap has:
data.test set to true
(filters are skipped). Use it to build your receiver against every payload shape.A contact was added to the user's account — from the contacts page, by sharing a report with a new recipient, or through a home-value lead form.
| X-CMAsnap-Event required | string The event name |
| X-CMAsnap-Webhook-Id required | string ID of the webhook that sent this delivery |
| X-CMAsnap-Signature required | string
|
| event required | string Value: "contact.created" |
| createdAt required | string <date-time> When the event happened |
required | object |
{- "event": "contact.created",
- "createdAt": "2026-09-30T16:40:25.000Z",
- "data": {
- "contact": {
- "id": "3f1c2b9e-8d4a-4a4e-9f2e-1b7c6d5e4a3b",
- "name": "Jane Smith",
- "emails": [
- "jane@example.com"
], - "phones": [
- "+15125550123"
], - "createdAt": "2026-09-30T16:40:25.000Z",
- "updatedAt": "2026-09-30T16:40:25.000Z"
}
}
}A contact's name, emails, or phone numbers changed. Other changes don't send this event.
| X-CMAsnap-Event required | string The event name |
| X-CMAsnap-Webhook-Id required | string ID of the webhook that sent this delivery |
| X-CMAsnap-Signature required | string
|
| event required | string Value: "contact.updated" |
| createdAt required | string <date-time> When the event happened |
required | object |
{- "event": "contact.updated",
- "createdAt": "2026-09-30T17:02:11.000Z",
- "data": {
- "contact": {
- "id": "3f1c2b9e-8d4a-4a4e-9f2e-1b7c6d5e4a3b",
- "name": "Jane Smith",
- "emails": [
- "jane@example.com",
- "jane.smith@work.example.com"
], - "phones": [
- "+15125550123"
], - "createdAt": "2026-09-30T16:40:25.000Z",
- "updatedAt": "2026-09-30T17:02:11.000Z"
}
}
}A contact (or a not-yet-identified visitor) engaged with one of the user's reports.
activity |
When |
|---|---|
report_shared |
The user sent a report to a recipient by email or text |
report_opened |
A recipient opened a shared report link. openCount is the running total for that link |
qr_scan |
Someone scanned a report's QR code. They aren't identified yet, so recipient and contactId are null |
homevalue_opt_in |
A visitor opted in on a home-value page. level is first or second |
Use trackingId + openCount to deduplicate repeated opens.
| X-CMAsnap-Event required | string The event name |
| X-CMAsnap-Webhook-Id required | string ID of the webhook that sent this delivery |
| X-CMAsnap-Signature required | string
|
| event required | string Value: "contact.activity" |
| createdAt required | string <date-time> When the event happened |
required | object |
{- "event": "contact.activity",
- "createdAt": "2026-09-30T18:15:00.000Z",
- "data": {
- "activity": "report_opened",
- "recipient": "jane@example.com",
- "contactId": "3f1c2b9e-8d4a-4a4e-9f2e-1b7c6d5e4a3b",
- "reportId": "9b2e4c1a-6f3d-4e8a-b1c2-d3e4f5a6b7c8",
- "trackingId": "AB12CD",
- "openCount": 2,
- "address": "123 Main St, Austin, TX 78701"
}
}A report finished generating and is ready to view — including reports created through the Reports API or an AI assistant.
| X-CMAsnap-Event required | string The event name |
| X-CMAsnap-Webhook-Id required | string ID of the webhook that sent this delivery |
| X-CMAsnap-Signature required | string
|
| event required | string Value: "report.generated" |
| createdAt required | string <date-time> When the event happened |
required | object |
{- "event": "report.generated",
- "createdAt": "2026-09-30T16:41:02.000Z",
- "data": {
- "report": {
- "id": "9b2e4c1a-6f3d-4e8a-b1c2-d3e4f5a6b7c8",
- "address": "123 Main St, Austin, TX 78701",
- "reportType": "Residential",
- "datasetId": "actris",
}
}
}CMAsnap exposes a Model Context Protocol (MCP) server so AI assistants such as Claude, ChatGPT, Gemini, Perplexity, and other MCP clients can create and analyze CMA reports on a user's behalf.
Server URL: https://app.cmasnap.com/mcp (Streamable HTTP transport)
Most users never need anything but the server URL — the assistant handles sign-in automatically. Step-by-step instructions for each assistant are also on the Integrations → AI Assistants page inside CMAsnap.
Claude (claude.ai / Claude Desktop): Settings → Connectors → Add custom connector → paste the server URL.
ChatGPT: Settings → Apps → Advanced settings → enable Developer mode, then Settings → Connectors → Create, name it "CMAsnap", paste the server URL, and choose OAuth.
Gemini: Settings & help → Connected apps → Add a custom app → paste the server URL.
Perplexity: Account settings → Connectors → name it "CMAsnap", paste the server URL, Authentication OAuth, Transport Streamable HTTP.
Claude Code:
claude mcp add --transport http cmasnap https://app.cmasnap.com/mcp
Any client using a JSON config:
{
"mcpServers": {
"cmasnap": {
"type": "http",
"url": "https://app.cmasnap.com/mcp"
}
}
}
| Tool | Description |
|---|---|
list_reports |
List the user's reports (limit 1–100, offset) |
create_report |
Create a report from a street address (optional reportType) |
create_reports |
Create reports for up to 50 addresses at once; returns created and failed lists |
create_report_by_listing |
Create a report from an MLS listing ID |
get_report_status |
Poll a new report until status is ready (queued / ready / failed, with progress) |
get_report |
Full report as markdown: subject, comparables, adjustments, market analysis, valuation |
get_report_properties |
Nearby market properties, 50 per page |
get_report_images |
Property photos as inline images the assistant can see (subject first, then comparables) |
generate_report_summary |
AI-written professional summary of the report |
ask_report_question |
Ask a question about a report (up to 2,000 characters) |
Reports are generated in the background, so a typical flow is create_report →
get_report_status (repeat until ready) → get_report.
| jsonrpc required | string Value: "2.0" |
| method required | string MCP method (e.g. |
| params | object |
required | string or integer |
{- "jsonrpc": "2.0",
- "id": 1,
- "method": "tools/call",
- "params": {
- "name": "create_report",
- "arguments": {
- "address": "123 Main St, Austin, TX 78701"
}
}
}{ }