Property Enrichment
Skip-trace a property to get owner contact info — names, phones, and emails. Enrich one property on demand, or queue a bulk job for up to 50,000. You're charged only for properties that return a match; cache hits and no-matches are free.
Property Enrichment looks up owner contact information for a property — names, phone numbers, and emails for the people associated with the parcel. Give it a property ID and it returns the matched person records, drawn from the property's canonical address (falling back to an associated loan or sale address when the canonical one is missing).
There are two ways to use it:
- Enrich one property —
POST /v1/properties/{id}/enrich. Synchronous; returns the contact data (or a no-match) in the response. Best for on-demand lookups in a UI or a per-record workflow. - Bulk enrich —
POST /v1/properties/enrich-bulk. Queues a background job for up to 50,000 properties and returns ajobIdyou poll. Best for list-building and batch pipelines.
Enrichment runs on property records. Use the list/get property endpoints to find the IDs you want to enrich first.
How credits work
Like the rest of the API, enrichment is metered in credits, with a simple rule worth understanding before you call it:
- You're charged only for a property that returns a match. A "match" means the skip-trace ran live and came back with at least one person.
- Cache hits are free. Once a property has been enriched, repeat calls are served from cache and cost nothing.
- No-matches are free. If the live skip-trace runs but finds no contacts, you are not charged.
refresh=truebypasses the cache and re-charges. Use it only when you deliberately want fresh data — it forces a live lookup and bills again on a match.
Every single-enrich response tells you exactly what happened through two fields — charged (did this call debit a credit?) and reason (which path produced the result). They line up one-to-one:
reason | What happened | Charged? |
|---|---|---|
fresh_skip_trace | Live lookup, returned ≥1 person | Yes |
fresh_skip_trace_empty | Live lookup, no contacts found | No |
member_cache_hit | You already enriched this property | No |
Any other reason value indicates a cache hit and is never charged. In short: charged: true happens only on reason: "fresh_skip_trace". Pair charged with reason to drive your billing UX, and pair reason with the presence of data.persons to drive your empty-state UX.
Enrich one property
Send a POST to /v1/properties/{id}/enrich. Authenticate exactly like any other endpoint — an x-api-key header or a Bearer token (see Authentication).
curl -X POST https://api.modelmatch.com/v1/properties/PROP_ID/enrich \
-H "x-api-key: mm_your_key_here"A successful response wraps the contact data alongside the billing metadata:
{
"data": {
"persons": [
{
"name": { "first": "Jane", "last": "Doe", "full": "Jane Doe" },
"phoneNumbers": [
{ "number": "+13105550142", "type": "mobile", "reachable": true, "dnc": false, "score": 92 }
],
"emails": [{ "email": "jane.doe@example.com" }],
"propertyAddress": { "street": "123 Main St", "city": "Los Angeles", "state": "CA", "zip": "90012" },
"mailingAddress": { "street": "123 Main St", "city": "Los Angeles", "state": "CA", "zip": "90012" }
}
],
"enrichedAt": "2026-06-25T18:40:00.000Z",
"source": "skip-trace"
},
"cached": false,
"charged": true,
"reason": "fresh_skip_trace",
"result": { "requested": 1, "matched": 1, "noMatch": 0, "error": 0 }
}Response fields
| Field | Type | Description |
|---|---|---|
data.persons | array | Matched people. Each carries name, phoneNumbers, emails, propertyAddress, mailingAddress, and flags like dnc / litigator / death. Empty on a no-match. |
data.enrichedAt | string | ISO-8601 timestamp of when the data was produced. |
data.source | string | Identifier for the data source. |
cached | boolean | Whether the result came from cache. |
charged | boolean | true only when this call debited a credit (see How credits work). |
reason | string | The pipeline path used — one of the values in the table above. |
result | object | Per-call counts: requested, matched, noMatch, error. |
Refreshing
By default, a property that has already been enriched is served from cache (free). Pass refresh=true to bypass the cache, force a live skip-trace, and re-charge on a match:
curl -X POST "https://api.modelmatch.com/v1/properties/PROP_ID/enrich?refresh=true" \
-H "x-api-key: mm_your_key_here"refresh=true will debit a credit again if the fresh lookup matches. Only use it when you specifically want newer data than the cache holds.
Bulk enrich
To enrich many properties at once, submit a job with the property IDs. The job runs in the background and enriches up to 50,000 properties per submission.
curl -X POST https://api.modelmatch.com/v1/properties/enrich-bulk \
-H "x-api-key: mm_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"propertyIds": ["PROP_1", "PROP_2", "PROP_3"]
}'The call returns 202 Accepted immediately with a job handle:
{
"jobId": "job_abc123",
"status": "queued",
"propertyCount": 3,
"estimatedCost": 3
}| Field | Description |
|---|---|
jobId | Handle to poll for status. |
status | queued on submission. |
propertyCount | Number of IDs accepted. |
estimatedCost | Worst-case credit cost, assuming every property matches. The actual debit is per match — no-matches and cache hits don't charge — so the real total is usually lower. |
| Request field | Type | Description |
|---|---|---|
propertyIds | string[] | Required. Property IDs to enrich. Hard cap of 50,000. |
refresh | boolean | When true, bypasses the cache for every property and re-charges on each match. Default false. |
Bulk jobs are gated on the worst case, but charged per match. Submission requires enough balance to cover every property matching, even though you're only billed for properties that actually return a match. Size jobs against your balance, not your expected match rate.
Poll for status
Poll GET /v1/properties/enrich-bulk/{jobId} until status is completed (or failed):
curl https://api.modelmatch.com/v1/properties/enrich-bulk/job_abc123 \
-H "x-api-key: mm_your_key_here"{
"jobId": "job_abc123",
"status": "completed",
"total": 3,
"succeeded": 3,
"failed": 0,
"fromCache": 1,
"chargedCount": 2,
"startedAt": "2026-06-25T18:40:00.000Z",
"completedAt": "2026-06-25T18:41:12.000Z",
"resultsUrl": "https://..."
}| Field | Description |
|---|---|
status | queued, running, completed, or failed. |
total / succeeded / failed | Row counts. |
fromCache | Rows served from cache (not charged). |
chargedCount | Rows that actually debited a credit. This is the precise debited total; succeeded - chargedCount is the no-match count. |
resultsUrl | Present once completed — a download link for the enriched results. It's re-signed on every status fetch, so always use the latest one. |
Instead of polling, you can subscribe to the enrichment.bulk.* events to be notified on completion. Job records are retained for 30 days; after that, GET /enrich-bulk/{jobId} returns 404.
List your enrichments
GET /v1/properties/enrichments returns the properties you've already enriched, paginated with a cursor (see Pagination):
curl https://api.modelmatch.com/v1/properties/enrichments \
-H "x-api-key: mm_your_key_here"{
"data": [
{ "propertyId": "PROP_1", "enrichedAt": "2026-06-25T18:40:00.000Z", "source": "skip-trace", "personCount": 2 }
],
"cursor": "eyJ..."
}Errors
All endpoints return the standard authentication errors (401, 403, 429). In addition:
| Status | Endpoint | Meaning |
|---|---|---|
400 | enrich | The property has no usable address on the parcel or any associated record, so it can't be skip-traced. |
400 | enrich-bulk | Validation error — e.g. an empty or oversized propertyIds. |
402 | enrich | Insufficient credits. The body includes your balance and the cost. |
402 | enrich-bulk | Insufficient credits — balance is below the worst-case cost. Body includes balance and cost. |
404 | enrich | Property not found. |
404 | enrich-bulk status | Job not found, or expired past the 30-day retention window. |
502 | enrich / enrich-bulk | The enrichment provider errored, or the bulk job failed to launch. |
A 402 is the one to handle gracefully — catch it, surface the balance vs cost to the user, and prompt them to top up before retrying.
Instant Search
Fast typeahead lookups across agents, originators, companies, and offices — call the route directly, or mint a scoped token and query the search host yourself.
Bulk Delivery
Export every record matching a list query to a single file — NDJSON, JSON, CSV, or Parquet. Bulk Delivery queues a background job that dumps the full result set to S3 and hands back a signed download URL. You're billed by the number of rows delivered.