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:

  1. Enrich one propertyPOST /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.
  2. Bulk enrichPOST /v1/properties/enrich-bulk. Queues a background job for up to 50,000 properties and returns a jobId you 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=true bypasses 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:

reasonWhat happenedCharged?
fresh_skip_traceLive lookup, returned ≥1 personYes
fresh_skip_trace_emptyLive lookup, no contacts foundNo
member_cache_hitYou already enriched this propertyNo

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

FieldTypeDescription
data.personsarrayMatched people. Each carries name, phoneNumbers, emails, propertyAddress, mailingAddress, and flags like dnc / litigator / death. Empty on a no-match.
data.enrichedAtstringISO-8601 timestamp of when the data was produced.
data.sourcestringIdentifier for the data source.
cachedbooleanWhether the result came from cache.
chargedbooleantrue only when this call debited a credit (see How credits work).
reasonstringThe pipeline path used — one of the values in the table above.
resultobjectPer-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
}
FieldDescription
jobIdHandle to poll for status.
statusqueued on submission.
propertyCountNumber of IDs accepted.
estimatedCostWorst-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 fieldTypeDescription
propertyIdsstring[]Required. Property IDs to enrich. Hard cap of 50,000.
refreshbooleanWhen 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://..."
}
FieldDescription
statusqueued, running, completed, or failed.
total / succeeded / failedRow counts.
fromCacheRows served from cache (not charged).
chargedCountRows that actually debited a credit. This is the precise debited total; succeeded - chargedCount is the no-match count.
resultsUrlPresent 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:

StatusEndpointMeaning
400enrichThe property has no usable address on the parcel or any associated record, so it can't be skip-traced.
400enrich-bulkValidation error — e.g. an empty or oversized propertyIds.
402enrichInsufficient credits. The body includes your balance and the cost.
402enrich-bulkInsufficient creditsbalance is below the worst-case cost. Body includes balance and cost.
404enrichProperty not found.
404enrich-bulk statusJob not found, or expired past the 30-day retention window.
502enrich / enrich-bulkThe 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.

On this page