Developer API · v1

API Documentation

Search 150M+ U.S. property records (153.9M in the verified July 2026 inventory), build a source-matched owner list, and pull it as JSON or CSV — the same engine that powers the PropContact dashboard. Search and preview are free; you spend one credit per record you pull.

Choose the reference that fits your workflow

  • This page is the readable guide, with billing rules, safety notes, and complete examples.
  • Swagger lets developers inspect the live contract and try entitled routes interactively.
  • ReDoc is a clean, searchable rendering of the same live contract.
  • OpenAPI JSON is the machine-readable source for SDK and client generation.

Quickstart

Three calls to your first list

Estimate the cost, build the list (it can wait for you), download the CSV.

bash
KEY='<PROPCONTACT_API_KEY>'

# 1 · estimate (free)
curl https://api.propcontact.net/v1/preview -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"filters":{"areas":[{"state":"NJ","city":"Cresskill"}],"has_wireless_phone":true},"max_results":40}'
# -> {"total_matching":3080,"eligible_for_export":3080,"estimated_credit_cost":40}

# 2 · build + wait (spends credits)
RESP=$(curl "https://api.propcontact.net/v1/lists?wait=55" -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Cresskill mobiles","filters":{"areas":[{"state":"NJ","city":"Cresskill"}],"has_wireless_phone":true},"max_results":40}')
# -> {"list_id":"7087fe64-…","status":"ready","credits_held":40}

LIST_ID=$(printf '%s' "$RESP" | jq -r '.list_id // empty')
test -n "$LIST_ID" || { printf '%s
' "$RESP" >&2; exit 1; }

# 3 · download the CSV (free)
curl -sL https://api.propcontact.net/v1/lists/$LIST_ID/download -H "Authorization: Bearer $KEY" -o list.csv

Authentication

API keys

Every request carries a Bearer key, created in your dashboard under Developer API. The full key is shown once at creation — store it in a secret manager.

http
Authorization: Bearer <PROPCONTACT_API_KEY>
  • pc_live_… — production. Search/preview/detail are free; list builds and billable enrichment results spend credits.
  • pc_test_…sandbox. Free reads hit real data, but builds return a fake list and never spend credits. Wire up your integration with this first.

Scopes

ScopeGrants
search:readsearch, preview, properties, dropdowns
lists:readlist status, items, download
lists:writePOST /lists (spends credits)
enrichment:writeLive async polling; paid sync after separate activation, for exactly entitled partner accounts

Headers

Request & response headers

HeaderDirectionMeaning
AuthorizationrequestRequired. Bearer <PROPCONTACT_API_KEY>
Content-Typerequestapplication/json on POST bodies
Idempotency-KeyrequestRequired on POST /enrichments and on paid POST /append after activation; recommended on POST /lists. Reuse only for an identical retry
RateLimit-LimitresponseYour per-key budget per window (default 120)
RateLimit-RemainingresponseRequests left in the current window
Retry-AfterresponseSeconds to wait — sent on 429

Credits

What costs credits & checking your balance

Free (rate-limited): me, usage, search, preview, properties, dropdowns.

Spends credits: POST /lists and live async enrichment hold credits and settle their actual billable rows. After its separate activation notice, paid POST /append directly settles one credit only when a phone or email is returned. Nonmatches and contact-free matches cost zero. Reading an already built list or completed batch is free.

bash
curl https://api.propcontact.net/v1/me -H "Authorization: Bearer $KEY"
# {"account_id":"…","credits_balance":123734,"active_plan_id":null,
#  "key":{"environment":"live","scopes":["search:read","lists:read","lists:write"],"rate_limit_per_min":120}}

When the balance cannot cover a write, POST /lists returns 402 insufficient_credits with detail.meta.{required, available, shortfall}; paid /append returns detail.{required, available}.

Reference

Endpoints

GET/v1/mefree

Account id, credit balance, key scopes

GET/v1/usagefree

Rolling 30-day usage summary

POST/v1/searchfree · search:read

Count + property summaries + map pins (pins carry only id + lat/lng)

POST/v1/previewfree · search:read

Estimate matches + credit cost

POST/v1/listsspends credits · lists:write

Build a contact list. ?wait=<secs> to block until ready; Idempotency-Key to be safe on retry

GET/v1/listsfree · lists:read

Your lists

GET/v1/lists/{id}free · lists:read

One list's status: pending·building·ready·failed·cancelled

GET/v1/lists/{id}/itemsfree · lists:read

Paginated rows (owner, Mobile_Phone1, Landline1, Email1, …)

GET/v1/lists/{id}/downloadfree · lists:read

The whole list as a CSV file

GET/v1/properties/{id}free · search:read

Single-property detail + contact availability flags (no raw numbers)

GET/v1/dropdowns/{STATE}free · search:read

City / county / ZIP lists for a 2-letter state

POST/v1/append0 or 1 credit after activation

Pending paid cutover: exactly one address; Idempotency-Key required; external_id echoed

POST/v1/enrichmentsholds credits · enrichment:write

Submit a durable address batch for polling; Idempotency-Key required

GET/v1/enrichments/{job_id}free · enrichment:write

Poll batch status and settled-credit totals

GET/v1/enrichments/{job_id}/resultsfree · enrichment:write

Page completed results with an opaque cursor

DELETE/v1/enrichments/{job_id}free · enrichment:write

Request early deletion after a supported terminal state

Filters

Every search/preview/build takes the same filters object.

GroupFields
Locationareas:[{state,city?|county?|zip?}] (preferred) · states[] counties[] cities[] zips[] zip_prefix
Propertyproperty_types[] bedrooms_min/max bathrooms_min/max sqft_min/max year_built_min/max is_commercial
Value / equityest_value_min/max equity_pct_min/max is_free_clear mortgage_balance_min/max last_sale_date_before/after
Ownerowner_corporate is_trust_owned cash_buyer owner_last_name is_senior_owner age_owner1_min/max
Contacthas_contact has_wireless_phone has_landline_phone has_email has_verified_phone exclude_dnc
Combinegroups:[Filters,…] + groups_op:"or"|"and"

CLEC does not mean mobile. CLEC is a source carrier classification, reported separately from wireless, landline, and any independently verified line status. It does not prove that a number is mobile or currently connected. For any source-matched number, use has_contact:true.

Partner enrichment

Address append and batch enrichment

Async batches are live; the paid single-address sync cutover is pending

During key creation, select enrichment:write. PropContact must also entitle the exact account; adding the scope to an ordinary key does not grant product access. Durable asynchronous jobs and separately entitled status-only webhooks are live. The one-address paid sync contract below is a release candidate until PropContact confirms schema 57 deployment and a production zero/one-credit canary. Do not route production sync traffic before that confirmation.

Enrichment accepts structured property addresses and returns one terminal result for every input. Your optional external_id and PropContact's input_index are echoed on every matched, not-found, ambiguous, or invalid row so you can join results without relying on a property ID.

Paid single-address append

bash
curl https://api.propcontact.net/v1/append -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"records":[{"external_id":"crm-row-42","address":{"street":"101 Main St","unit":"2B","city":"Cresskill","state":"NJ","zip":"07626"}}]}'

POST /v1/append accepts exactly one records[] item. A live request requires an active account entitlement and Idempotency-Key. It settles one credit only when at least one phone or email is returned; not-found, ambiguous, invalid, and contact-free matches cost zero. Contact values are released only after settlement. An identical retry never debits twice. Use /v1/enrichments for every multi-row request. Test keys return one deterministic synthetic record, never read production addresses, and never spend credits.

Activation gate: this paid sync shape replaces the completed complimentary benchmark contract only after PropContact sends an explicit activation notice. Until then, use the live paid /v1/enrichments flow for address work.

Durable batches

MethodPathPurpose
POST/v1/enrichmentsSubmit 1-10,000 addresses; Idempotency-Key is required
GET/v1/enrichments/{job_id}Poll queued/processing/complete/failed state and accounting totals
GET/v1/enrichments/{job_id}/resultsRead up to 1,000 typed results/page using the opaque next_cursor
DELETE/v1/enrichments/{job_id}Request early deletion after a complete/failed/cancelled terminal state
json
{
  "records": [
    {
      "external_id": "crm-row-42",
      "address": {
        "street": "101 Main St", "unit": "2B", "city": "Cresskill",
        "state": "NJ", "zip": "07626"
      }
    }
  ],
  "options": {
    "email_verification": false,
    "line_lookup": false,
    "compliance_scrub": false
  }
}

Send a unique visible-ASCII Idempotency-Key for each logical batch and reuse it only for an identical retry. Poll status until it is complete, then page results untilnext_cursor is null. Inputs/results are scheduled for deletion after 14 days; minimal accounting facts follow the financial retention schedule.

Polling remains authoritative: separately entitled accounts may add webhook: { url, signing_secret? } for a signed status-only notification. Webhooks contain no contacts or input rows; fetch results from the authenticated results endpoint.

Matching and credits

ResultMeaningBase charge
matchedOne exact normalized situs-address candidate1 only if a requested usable contact is returned
not_foundNo exact candidate0
ambiguousMultiple candidates or a required unit is missing0
invalidRequired address elements are malformed/missing0

An async batch holds at most one base credit per input, then settles only unique matched properties that contain a requested usable contact. Unused holds are restored. Duplicate inputs fan out to every caller row but charge once within that batch. Separate batches may be charged again under the partner freshness agreement; enrichment never silently removes a row because it appeared in an older list.

Contact metadata

Phones are returned wireless-first, followed by other source classifications. Each number may include E.164 normalization, source classification, identity-match quality/confidence/score, source DNC status, and connected status. A strong match means the source linked the number to this property's record on surname plus address or name, at the stated confidence. It is not identity verification, and it is not proof that the line is active, connected, reachable, mobile, or safe to contact. CLEC remains a separate source classification. A false or missing source DNC value is unknown, never "clear."

Base emails are unverified. Fresh carrier/LRN, provider-backed email validity, live line status, reassigned-number, DNC, and TCPA-litigator signals are unavailable in the initial release. Requests that enable email_verification, line_lookup, or compliance_scrub fail closed until a contracted provider, metering unit, data-processing terms, and account entitlement are enabled.

Completion notification

A webhook requires a public HTTPS callback on port 443 and separate exact-account entitlement. Supply a 32–256 character signing secret, or store the generated secret returned once on a fresh create. PropContact sends X-PropContact-Event-Id, X-PropContact-Delivery-Attempt, X-PropContact-Timestamp (Unix seconds), and X-PropContact-Signature in the form v1=<lowercase 64-hex HMAC-SHA256>.

text
signed_bytes = X-PropContact-Timestamp + "." + exact_raw_request_body
expected = "v1=" + hex(HMAC-SHA256(signing_secret, signed_bytes))

Verify the signature against the exact raw body before parsing JSON, compare it in constant time, reject a timestamp more than five minutes from your clock, require the body event_id to equal X-PropContact-Event-Id, and atomically deduplicate that event ID. A valid duplicate should return 2xx without doing the work twice. Transport failures, 408, 425, 429, and 5xx retry for about 32.5 hours; redirects are never followed. Always retain polling as the recovery path.

Operating

Error codes

Every error responds {"detail":{"code","message"}}. Switch on detail.code.

HTTPcodeMeaning
400bad_requestmalformed request
400max_results_too_largeabove the per-build cap (50,000)
400no_eligible_rowsfilters match nothing in that area
401unauthorizedmissing / invalid / revoked key
402insufficient_creditsbalance can't cover the write; list details are under meta, sync details are directly under detail
403missing_scopekey lacks the required scope
403append_not_entitledaccount lacks an active paid append entitlement
404not_foundproperty / list not found
409all_already_pulledevery match was already pulled by your account
409request_in_progressidempotency key still processing
422validation_errorinvalid fields — see detail.errors[]
409 / 422idempotency_key_reusedsame key, different body; status depends on write route
400idempotency_key_requiredadd a 1-200 visible-ASCII key to append/async submission
429append_daily_row_quota_exceededaccount UTC-day sync quota exhausted
503append entitlement / settlement / invariant failureno contacts were released; retry the same key
503enrichment_result_invariant_failedresult page withheld by a billing/contact safety check
403enrichment_webhook_disabled / not_entitledomit webhook or request separate entitlement
422invalid_webhook_urluse a public HTTPS destination on TCP 443
422provider_option_unavailablekeep every optional provider flag false
429rate_limitedtoo many requests — honor Retry-After
503enrichment_submission_unavailablesubmission failed before acceptance — retry with backoff
503webhook_validation / configuration_unavailableno hold was created — retry or contact support
500internal_errorserver error — retry with backoff

Rate limits, idempotency & sandbox

Rate limits. Default 120 requests / 60 s per key (higher on request). Over the limit → 429 rate_limited with Retry-After: 60. Sleep, then retry.

Idempotency. Live POST /append and POST /enrichments require Idempotency-Key; POST /lists strongly recommends it. Generate one key per logical write and reuse it only for an identical retry. This prevents duplicate receipts, jobs, and charges.

Sandbox. A pc_test_… key lets you build integrations for free — POST /lists returns a fake list and entitled POST /append returns deterministic synthetic people. Free search reads use real data. Async POST /enrichments accepts live keys only.

For AI agents & tools

Integrate in your app

Point your AI coding agent at this contract and it can wire the API into your app. The full spec with drop-in Python & TypeScript client classes is in our interactive reference. The core loop: preview (cost) → lists?wait= (build) → items / download.

python
import os, uuid, requests
BASE, KEY = "https://api.propcontact.net/v1", os.environ["PROPCONTACT_API_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
filters = {"areas": [{"state": "NJ", "city": "Cresskill"}], "has_wireless_phone": True}

r = requests.post(f"{BASE}/lists?wait=55",
    headers={**H, "Idempotency-Key": str(uuid.uuid4())},
    json={"filters": filters, "max_results": 40}).json()
rows = requests.get(f"{BASE}/lists/{r['list_id']}/items",
    params={"page_size": 500}, headers=H).json()["rows"]
print(len(rows), "contact records")
Acceptable use: lawful B2B only; no FCRA-regulated decisions (credit, employment, tenant screening); comply with TCPA and the Do-Not-Call registry when contacting people. Raw-data resale or redistribution is prohibited unless a signed Enterprise Order Form expressly permits it. Full terms at propcontact.net/api-terms.