Skip to content
Enrichment API · v1

Append PII to any phone.

POST a list of phone numbers and get back names, demographics, addresses, emails, and the IP plus source URL we carry, all resolved against our own identity graph. No third-party API-uptime dependency. You query our warehouse directly.

One request, up to 100 phones

Batch a request and get a row per phone back in the same call.

Counts only, never your data

We log how many phones matched for billing. We never store your phones or the appended PII.

Simple key auth

One header. Create, pause, or revoke keys yourself on the Developers page.

What each endpoint returns

EndpointBuilt forReturnsTypical speed
POST /api/v1/resolveReal time (sub-second): live call routing, bidding, pre-qualificationOne identifier in (phone or hashed email). Modes: enrich (~24 core fields: identity, address, age, income + credit bands, homeowner, home value, best contacts), rtb (a single pass/fail against your filters, no PII), identity (hashes, IP, source URL).typically a few hundred milliseconds round trip over the public internet; identifiers outside the hot index fall back to a deeper lookup (phones and sha256 emails resolve in well under a second; md5 is a slower legacy path, so prefer sha256 for hashed email)
POST /api/v1/enrichBatch append: up to 100 phones, or up to 100 full lead records, per callSend phones for phone-keyed append, send records (phone, email, name, address in any mix) to resolve the confirmed person, send vins to resolve a vehicle to the person behind it, or send addresses to get that property’s record straight back with no identity lookup in the path. Identity seeds return core identity + demographics + best contacts, PLUS optional field groups: ~160 curated consumer attributes, ~165 property fields, financial, vehicles with VIN, executive roles, geo precision, all contacts, and purchase behavior. VIN note: our vehicle file covers roughly 2020 and older, so newer vehicles return vehicle_not_on_file. Property accuracy: every property answer carries property_provenance saying which key it matched on and how confident we are. Send the address or ZIP you already hold on a phone row and a property that contradicts it is withheld rather than returned. Address-keyed lookup is the accurate path when you already know the address.a few seconds for a full 100-record batch (core fields); allow up to ~10 s with every field group attached
POST /api/v1/pullList building: homeowner lists by ZIP, on demandZIPs + filters (home value, year built, owner age, phone/email requirements) + a required record cap in; a queued order out. The CSV is emailed to the account and downloadable by key. Delivered records count toward the account's monthly plan.async; typically a few minutes to the file
POST /api/v1/append-phoneContact discovery: email or name+address in, phones outThe matched person's identity block, a match confidence grade, and every phone on file (type, rank, carrier, last seen, DNC status, and a single best-to-dial flag).1 to 3 s per batch
GET /api/v1/usageCheck your own usage and remaining limit from codeThis month and the last 7 days of records matched (leads) and total lookups across all your keys, plus your monthly record limit and how many records remain. A null limit means no fixed cap is set on your account.instant
GET /api/v1/statusCheck whether your feeds and pulls are running, from codeThe same data as the account Overview tab, keyed by API key instead of a login: records in/out by day over the window, and every feed, campaign, and pixel stream with its status (active, stale, failing, paused). ?days=7|30|90, default 30.instant
GET + POST /api/v1/website-visitorsGet your pixel data out, with email and phoneGET counts the people your pixel resolved (optionally filtered by state, ZIP, age, income, page, recency, conversion). POST the same filters with max_records and get a CSV download link: name, email, address, visit history, and phone with its type, DNC flag, and person-or-household match.counts instant; file in seconds
POST /api/v1/property-pullHomeowner lists by property: value, year built, equityestimate: true returns a free funnel of counts down to callable mobiles. max_records queues the order. DNC is delivered as a flag column, never filtered out.estimate in seconds; order async
POST /api/v1/aged-optinDirect mail: aged opt-in auto records by vehicle year, make, modelmode: estimate counts mailable households (with phone, email, VIN, and a per-year breakdown). mode: order queues the mail file.estimate in seconds; order async
GET /api/v1/pull/{id}/downloadCollect a finished order from codeThe CSV for any completed order, byte-for-byte the file that was emailed.instant
GET /api/v1/topics/searchFind what you can target before you buy itSearch roughly 50,000 targetable topics by keyword; get the topic ids a pull accepts, with reach. Free, no records.instant
GET /api/v1/feedsSee what each of your feeds deliversEvery feed on the account described in plain English: what it delivers, the source mix, every filter, cap, schedule, dedup rule, destination, and recent changes.instant
POST /api/v1/feeds/changeAdjust a feed’s targeting safelyTwo steps: a preview with the measured volume impact and a confirm_token, then the apply. Today the age floor moves here.seconds
POST /api/v1/feeds/controlPause, resume, or cap a feedpause, resume, or set_daily_cap. Going live is owner-approved, and the response says when an approval was filed instead.instant
GET /api/v1/audience-deliveryProve what went out to each ad platform audiencePer audience, per day, net-new records delivered, with a per-day series. Counts only, never PII.instant
GET /api/v1/ad-spendReconcile ad spend against your invoiceWhat your connected ad accounts spent, by day, account, campaign, or platform, with settled and locked so you know which numbers are final.instant
GET + POST /api/v1/suppressionsKeep your do-not-contact list current from your own systemsPOST phones to add them; they are excluded from every pull and blocked on the realtime API from then on. GET the counts.instant
GET + POST /api/v1/activation/datasets, POST /api/v1/activation/datasets/{id}/in-marketYour own database: who in it is in market, and take those recordsCreate a database from records or a CSV (your own columns are kept, so one file can carry every client). Then mode: estimate counts who is in market for a topic where your column says X, free, and mode: pull returns those records with the current mobile, the topics, your columns and a List_ID, billed per record.count in seconds; pull in seconds to a minute
POST /api/v1/activation/datasets/{id}/recordsStream sign-ups into a connected database as they happenUp to 500 records per call, field names auto-detected, retries deduplicated. ?dry_run=1 checks everything and stores nothing.seconds
GET + POST /api/v1/revenueRevenue share: report what a list earnedPost a total or individual sales against the list_id on the file we sent. Never lost, never double counted, safe to re-post. GET reads back what you reported or the list ids you can report on.instant
/api/v1/roofradar/*RoofRadar accounts: lists and lookups for your territoryPull a homeowner list by ZIP with roofing filters, resolve one home to its roof facts and owner, download the CSV. Needs a RoofRadar plan (402 without one).resolve in seconds; lists async
POST /api/v1/identifyDeprecatedAn alias of enrich records mode kept for existing integrations. Use /api/v1/enrich with records.same as enrich

Speeds above are conservative planning figures that include real network time; most calls come back faster. The Playground shows your own latency from your network on every call.

Quickstart

Three steps to your first call.

  1. 1Sign up at talkdatatome.online/signup. Any plan works to start.
  2. 2Create a key on the Developers page. Copy it right away. The key is shown once and starts with rk_.
  3. 3Make a call with the key in the X-API-Key header. Pick your language below.

Prefer to skip straight to a running call? The API Playground runs every endpoint from your browser with your key, shows real latency, and hands you the exact cURL to reproduce it.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/resolve \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"identifier": "4155550142"}'
Python
import requests

resp = requests.post(
    "https://www.talkdatatome.online/api/v1/resolve",
    headers={"X-API-Key": "rk_your_key_here"},
    json={"identifier": "4155550142"},
)
print(resp.json())  # {"matched": true, "first_name": "...", ...}
Node.js
const resp = await fetch("https://www.talkdatatome.online/api/v1/resolve", {
  method: "POST",
  headers: {
    "X-API-Key": "rk_your_key_here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ identifier: "4155550142" }),
});
console.log(await resp.json()); // { matched: true, first_name: "...", ... }

That is the real-time /resolve endpoint, which answers a single identifier in well under a second. To append data to a whole list at once, use the batch /enrich endpoint below. Both take the same key.

Authentication

Every request must include your secret key in the X-API-Key header. Keys look like rk_… and are shown to you once at creation. Treat your key like a password and never expose it in client-side code. A missing, invalid, inactive, or expired key returns 401.

Create and manage keys yourself on the Developers page: issue a new key, pause or revoke any key, and watch this month's usage. The same key works for every endpoint on this page.

Header
X-API-Key: rk_your_key_here

OpenAPI spec

The full contract is published as an OpenAPI 3.1 document covering every key-authenticated endpoint on this page, with request and response schemas. Load it into Postman or Insomnia, or point a client generator at it to get a typed SDK in your language. No key is needed to read it.

Open the OpenAPI spec

Spec URL
https://www.talkdatatome.online/api/openapi.json

The spec is generated from the running code, so it always matches the deployed behavior. Signed in, you can also reach it any time from API in the top nav.

Enrich

POST/api/v1/enrich

Send one phone via {"phone": "..."} or up to 100 via {"phones": ["...", "..."]}. Phones can be in any common format. We normalize to 10 digits and de-duplicate before resolving. Every submitted phone comes back in results, matched or not.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/enrich \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"phones": ["+1 (415) 555-0142", "2025550173"]}'

Request body

{
  "phones": [
    "+1 (415) 555-0142",
    "2025550173"
  ]
}

Response

{
  "requested": 2,
  "matched": 1,
  "results": [
    {
      "phone": "+1 (415) 555-0142",
      "matched": true,
      "confidence": "high",
      "verified": false,
      "matched_by": "phone",
      "phone_people": 1,
      "first_name": "Jordan",
      "last_name": "Avery",
      "age": 47,
      "gender": "M",
      "marital_status": "Married",
      "homeowner": "Home Owner",
      "household_income": 112500,
      "address": "1240 Oak Ridge Dr",
      "city": "San Francisco",
      "state": "CA",
      "zip": "94110",
      "mobile_phone": "4155550142",
      "landline_phone": "4155550199",
      "email": "jordan.avery@example.com",
      "ip_address": "73.92.18.204",
      "source_url": "homeimprovementquotes.com"
    },
    {
      "phone": "2025550173",
      "matched": false
    }
  ]
}

Null fields are omitted from a matched result, so a record only carries the attributes we actually hold. An unmatched phone returns just {"phone": "...", "matched": false}. You are billed for matched rows only.

How sure we are, and asking for only the sure ones

Every matched phone carries confidence: high when exactly one person is on the number, it is their primary line, and we saw the pairing within the last 12 months; medium when it is their primary or secondary line seen within the last 24 months, or a second person is on the number and we returned the more recent one; low for everything else (a third or later line, seen more than 24 months ago or never dated, or three or more people on the number). It also carries phone_people, how many people our graph has on that number.

Our identity graph comes first. When it holds a verified pairing of the number and a person, from a source that checks identity before it records a number, that person is returned ahead of any other association we hold, the row says "verified": true, and the confidence is high whatever the other signals say. Every other match says "verified": false and is banded as above.

When the caller number is all you have, for example scoring a live inbound call, add "min_confidence": "high" (or "medium"). A match below the bar comes back as "matched": false with "reason": "below_min_confidence" and the band it had. It carries no data, does not count toward matched, and is not billed. Any other value is a 400.

Request body

{
  "phones": ["5303219112"],
  "min_confidence": "high"
}

Response

{
  "requested": 1,
  "matched": 0,
  "results": [
    {
      "phone": "5303219112",
      "matched": false,
      "reason": "below_min_confidence",
      "confidence": "low"
    }
  ]
}

Or send a full lead

When you already have more than a phone, send records instead of phones. Each record can carry any mix of phone, email, name, and postal address. We resolve on the strongest identifier present and return the confirmed person plus homeowner and household attributes. Sending name and address alongside the phone or email lifts the match rate versus phone alone. Up to 100 records per request.

Each record needs a last_name plus at least one of: a phone, an email, or a postal address with zip. The response tells you which identifier resolved each row in matched_by.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/enrich \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"phone": "4045550142", "email": "jane.doe@example.com", "first_name": "Jane", "last_name": "Doe", "address": "123 Main St", "city": "Atlanta", "state": "GA", "zip": "30301"}]}'

Request body

{
  "records": [
    {
      "phone": "4045550142",
      "email": "jane.doe@example.com",
      "first_name": "Jane",
      "last_name": "Doe",
      "address": "123 Main St",
      "city": "Atlanta",
      "state": "GA",
      "zip": "30301"
    }
  ]
}

Response

{
  "requested": 1,
  "matched": 1,
  "results": [
    {
      "matched": true,
      "matched_by": "phone",
      "confidence": "high",
      "verified": true,
      "first_name": "Jane",
      "last_name": "Doe",
      "address": "123 Main St",
      "city": "Atlanta",
      "state": "GA",
      "zip": "30301",
      "homeowner": "Home Owner",
      "is_homeowner": true,
      "line_type": "mobile",
      "dnc": 0,
      "age": 47,
      "gender": "F",
      "income_band": "H. $75,000-$99,999",
      "income_midpoint": 87500,
      "credit_range": "C. 700-749"
    }
  ]
}

The top-level matched count reflects how many records resolved. An unmatched record returns just {"matched": false}, and you are billed for matched rows only. Response fields are the same set documented below.

Response fields

FieldTypeDescription
phonestringThe phone number you submitted, echoed back.
matchedbooleanWhether the phone resolved to an identity.
confidencestring"high", "medium", or "low". How strongly the returned person is tied to this number. A verified pairing is always high. Otherwise high means exactly one person on the number, it is their primary line, and we saw the pairing within the last 12 months. Medium means their primary or secondary line seen within the last 24 months, or a second person on the number with the more recent one returned. Low is everything else: a third or later line, seen more than 24 months ago or never dated, or three or more people on the number.
verifiedbooleanTrue when our identity graph holds a verified pairing of this number and this person, from a source that checks identity before it records a number. A verified pairing is returned ahead of any other association we hold and is always high confidence. False means the match rests on the observed association alone.
matched_bystringAlways "phone" on this path: the phone number resolved the person.
phone_peoplenumberHow many distinct people our graph has on this number, the verified person included.
reasonstringPresent on an unmatched row when you sent min_confidence and the match fell below it: "below_min_confidence". The row also carries the confidence band it had, and it is not billed.
first_namestringGiven name on the resolved identity.
last_namestringFamily name on the resolved identity.
agenumberEstimated age.
genderstringGender on file.
marital_statusstringMarital status on file.
homeownerstringHome ownership status.
household_incomenumberEstimated household income midpoint.
addressstringStandardized street address.
citystringStandardized city.
statestringTwo-letter state code.
zipstringZIP code.
mobile_phonestringBest deliverable mobile number on the identity.
landline_phonestringBest landline on the identity.
emailstringBest email on the identity.
ip_addressstringIP address associated with the identity.
source_urlstringSource URL associated with the identity.

That is the default response. Every dataset in the graph, with its fields, types, and definitions, is documented in the data dictionary. No login needed, so it is safe to send to your team.

Field groups: the full data dictionary on batch enrich

The default response carries the core identity + demographic set above. Pass fields to nest entire data-dictionary sections on every matched row. Billing is unchanged: a matched row counts once no matter how many groups you request.

GroupReturnsShape
demographicsA curated ~160-attribute set from the consumer data tab: interests, purchase behavior, lifestyle, household composition, occupation, education, and more. Null-valued attributes are omitted per row. The full 2,000+ attribute tab is available through managed appends and file delivery.demographics: {...}
propertyThe property tab for the person's address: ~165 fields (year built, square footage, beds/baths, lot, pool, valuation, sale history, owner occupancy).property: {...}
financialThe financial tab: income, net-worth and investment indicators, credit activity.financial: {...}
autoThe consumer auto tab: up to 5 vehicles per person with VIN, make, model, year, body type, fuel, purchase date, and more.vehicles: [{...}]
geoTerritory precision for the person's address: latitude/longitude, county, DMA, MSA, CBSA, census tract and block group, congressional district, ZIP+4, urbanicity, plus birth month and year.geo: {...}
contactsEvery contact point on the person: up to 10 phones (type, rank, carrier, work-phone flag, DNC, quality, last seen) and up to 5 emails (rank, quality, opt-in, md5/sha256, register and update dates).contacts: {phones: [...], emails: [...]}
purchase_behavior~114 recent-purchase aggregates by category: dollars, orders, items, and company counts (apparel, home goods, donations, subscriptions, gourmet, and more).purchase_behavior: {...}
signalsLive in-market intent (rolling recent window): consumer and business topics the person is actively researching, with topic names, categories, frequency, and recency. Up to 25 of each per person.signals: {b2c: [...], b2b: [...]}
behaviorBehavioral interest codes (IAB) with recency, the weekly behavioral feed. Up to 50 per person.behavior: [{...}]
executiveThe B2B executive-at-home overlay: up to 3 roles with company, title, executive level, SIC/industry, revenue band, and business contact points. Present when the person is a linked business executive.executive_roles: [{...}]
Request with field groups
curl -X POST https://www.talkdatatome.online/api/v1/enrich \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phones": ["4155550142"], "fields": ["demographics", "property", "auto", "executive"]}'
Full example response (sample person, every group; field names are real)
{
  "requested": 1,
  "matched": 1,
  "results": [
    {
      "phone": "4155550142",
      "matched": true,
      "first_name": "Jordan",
      "last_name": "Avery",
      "age": 47,
      "gender": "M",
      "marital_status": "Married",
      "homeowner": "Home Owner",
      "household_income": 112500,
      "address": "1240 Oak Ridge Dr",
      "city": "San Francisco",
      "state": "CA",
      "zip": "94110",
      "mobile_phone": "4155550142",
      "landline_phone": "4155550199",
      "email": "jordan.avery@example.com",
      "ip_address": "73.92.18.204",
      "source_url": "homeimprovementquotes.com",

      "demographics": {
        "age": 47,
        "birth_year": 1979,
        "credit_range": "B. 750-799",
        "credit_midpts": 775,
        "dwelling_type": "Single Family",
        "length_of_residence": 9,
        "children_hh": 2,
        "child_aged_7_9_hh": "Y",
        "occupation_category": "Management",
        "education": "College",
        "net_worth_hh": "$250,000-$499,999",
        "home_improvement": "Y",
        "diy": "Y",
        "cooking": "Y",
        "dog_owner": "Y",
        "camping_hiking": "Y",
        "travel_affinity": "Y",
        "investments": "Y",
        "credit_card": "Y",
        "catalog_affinity": "Y",
        "veteran_hh": "N",
        "donor_affinity": "Y"
        // ... ~108 more attributes on a real match (nulls omitted)
      },

      "property": {
        "yr_built_orig": 1987,
        "living_sqft": 2140,
        "land_sqft": 6300,
        "nmbr_bedrooms": 4,
        "tot_baths_est": 2.5,
        "full_baths": 2,
        "half_baths": 1,
        "pool": "Y",
        "owner_occupied": "Y",
        "estimated_value": 918000,
        "assess_val_prop": 742000,
        "sale_date": "2017-08-14",
        "sale_amt": 685000,
        "county_name": "San Francisco"
        // ... ~150 more property fields on a real match
      },

      "financial": {
        "discretionary_income": 38500,
        "mortgage_refinance_intent": "Y",
        "mortgage_open1st_intent": "N",
        "automotive_loan_intent": "N",
        "bank_card_intent": "Y",
        "personal_loan_intent": "N",
        "card_offer_response": "Y",
        "mortgage_offer_response": "N"
      },

      "vehicles": [
        {
          "vin": "SAMPLEVIN0000TEST",
          "make": "Toyota",
          "model": "Tundra",
          "year": 2019,
          "fuel": "Gasoline",
          "msrp": 41500,
          "body_type": "Truck",
          "class": "Mainstream",
          "drive_type": "4WD",
          "engine_cylinders": 8,
          "transmission_type": "Automatic",
          "purchase_date": "2021-03-02",
          "purchase_new": "N",
          "number_of_vehicles_in_hh": 2
          // ... full vehicle spec on a real match; up to 5 vehicles
        }
      ],

      "executive_roles": [
        {
          "company_name": "Avery Roofing Co",
          "standardized_title": "Owner",
          "executive_level": "C-Level",
          "executive_department": "Executive",
          "primary_sic_description": "Roofing Contractors",
          "location_sales_description": "$1M-$2.5M",
          "location_employee_description": "10-19",
          "bus_address_city": "San Francisco",
          "bus_address_state": "CA",
          "bus_phone": "4155550171"
          // ... ~100 more executive/business fields; up to 3 roles
        }
      ]
    }
  ]
}

The example is trimmed where marked: a real response carries ~160 demographics attributes and ~165 property fields per match, with null attributes omitted. Signed-in accounts can browse the complete field-by-field catalog on the Data catalog page.

Heads-up on size: demographics is wide; a full 100-phone batch with every group can run a few megabytes of JSON. Request only the groups you need. Groups are served from our own store; the batch cap stays 100 per request.

Need an attribute you do not see? The curated demographics set is a config choice, not a data limit. Any attribute from the full dictionary can be promoted into the API response, usually same day. Tell us which fields you need and they will be in your next call.

Error codes

Errors return a JSON body shaped like {"error": "..."} with the matching HTTP status.

StatusMeaning
400Malformed request. Body is not valid JSON, the phone count is outside 1 to 100, or min_confidence is not "high" or "medium".
401Missing, invalid, inactive, or expired API key.
429Monthly enrichment cap reached for this key.
500Something failed on our side. Safe to retry shortly.
503The service is not fully configured. Contact us if this persists.

Rate limits

  • Up to 100 phones per request. Larger batches return 400. Split bigger jobs into multiple calls.
  • Each key can carry a monthly matched-row cap. Once you reach it, requests return 429 until the next calendar month. Only matched rows count toward the cap.
  • Need a higher cap or volume pricing? Reach out and we will size a plan to your usage.

List pulls by ZIP

Build a homeowner list programmatically instead of looking up records you already have. POST ZIPs, optional filters, and a required max_records cap; the pull runs asynchronously, the CSV is emailed to your account, and the file is downloadable over the API. ZIPs can be five digits, or a three-digit prefix to cover a whole metro area (for example "331" for the Miami area) — the same shorthand the estimate call accepts. The cap is a hard ceiling, and a pull is refused up front if it would exceed what your account can deliver, so a runaway job cannot surprise you. An order is never quietly shortened: if we cannot deliver the full max_records you get a 402 naming the number we can deliver, and nothing is pulled or charged. Every order carries a list_id, returned in this response and again once the pull completes — quote it if you ever report revenue back to us on a rev share account. Automation-only integrations that only ever poll and download (never read the notification email) can ask us to turn email delivery off entirely for the account; the file still lands in storage and download_url still works the same either way.

Queue a pull
curl -X POST https://www.talkdatatome.online/api/v1/pull   -H "X-API-Key: YOUR_KEY"   -H "Content-Type: application/json"   -d '{
    "zips": ["77066", "77069", "77070"],
    "max_records": 5000,
    "filters": {
      "min_home_value": 150000,
      "year_built_on_or_before": 2010,
      "phone_requirement": "mobile"
    }
  }'
# → 202 {"queued": true, "order_id": "...", "list_id": "ZIPLIST-...",
#        "cancel_url": "/api/v1/pull/<id>/cancel", "billing": {...}}

# ZIP-3 prefixes cover a whole metro in one entry instead of listing every ZIP:
curl -X POST https://www.talkdatatome.online/api/v1/pull   -H "X-API-Key: YOUR_KEY"   -H "Content-Type: application/json"   -d '{"zips": ["331", "332"], "max_records": 5000, "filters": {"phone_requirement": "none"}}'
# → 202 {"queued": true, "order_id": "...", "list_id": "ZIPLIST-...", "zips": 2, ...}

# Poll status + get the download link. Pages 20 at a time by default:
curl "https://www.talkdatatome.online/api/v1/pull?limit=20&offset=0" -H "X-API-Key: YOUR_KEY"
# → {"orders": [{"id": "...", "status": "completed", "record_count": 5000,
#                "download_url": "/api/v1/pull/<id>/download"}],
#    "limit": 20, "offset": 0, "total": 353, "has_more": true}

# Changed your mind? Cancel before it is delivered:
curl -X POST https://www.talkdatatome.online/api/v1/pull/<id>/cancel -H "X-API-Key: YOUR_KEY"
# → {"canceled": true, "billed": false, "delivered": false}

Filters go inside filters, never at the top level of the request: min_home_value, max_home_value, year_built_on_or_after, year_built_on_or_before, min_sqft, min_owner_age, max_owner_age, require_email, phone_requirement ("mobile", "any", or "none"). Up to 10,000 ZIPs (one call, however long the list) and 100,000 records per pull; bigger jobs run managed.

A key we do not recognise is reported, never dropped. Anything we do not read comes back in ignored_filters with a warning, and that includes keys sent at the top level of the request instead of inside filters. A top-level phone_requirement is not applied: it is echoed back so you can resend it nested rather than paying for a broader audience than you asked for.

Refused, not shortened. If your prepaid balance or your daily cap cannot cover the full max_records, the request returns 402 with records_deliverable, and nothing is pulled or charged. Lower max_records to that number, or resend with allow_partial: true to accept the shorter file on purpose. A max_records exactly equal to what we can deliver is a complete order and runs normally.

Which number to watch. Every 202 carries a billing block that names the meter your account is actually on. On a monthly plan it reports records_remaining, debited when you download the file. On a prepaid balance it reports balance_usd and records_deliverable instead, because a prepaid pull is charged against the balance and does not move plan records_remaining at all. If you are billed from a balance, that field stays where it is and is not the one to watch.

Listing your orders. GET /api/v1/pull returns newest first, 20 per page by default. Pass limit (1 to 100) and offset to page through the rest; the response carries total and has_more so you know when to stop.

Cancelling an order. POST /api/v1/pull/{id}/cancel (or DELETE on the same path). The 202 hands you order_id and cancel_url so you can cancel straight away without polling for the order first. Work starts the moment you order, so cancelling does not promise to stop a query already running, and we will not tell you it did. What it does guarantee is the part that costs you: a cancelled order is never charged, on any kind of pull, and its download link is disabled. A cancelled ZIP pull also stops before it builds or sends anything, so no file and no email. On a topic or property pull a file that was already built may still reach your inbox; you are not billed for it. Calling cancel twice is safe and returns the same answer. An order that already completed returns 409: the file was delivered, so we will not pretend otherwise. You can only cancel your own account's orders.

Bulk appends

The batch endpoints are deliberately capped at 100 records per request: it keeps latency flat, keeps any single failure small, and lets your integration stream results as they come back. For bulk jobs, loop your file in pages of 100. A one million row append is 10,000 sequential requests and typically finishes in under an hour from a single worker; run a few workers in parallel to go faster. Your monthly cap is enforced across all of them, and only matched rows count toward it.

Bulk loop (Node)
const rows = phones; // your full list
for (let i = 0; i < rows.length; i += 100) {
  const batch = rows.slice(i, i + 100);
  const resp = await fetch("https://www.talkdatatome.online/api/v1/enrich", {
    method: "POST",
    headers: { "X-API-Key": process.env.TDTM_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ phones: batch }),
  });
  const { results } = await resp.json();
  // append results to your output as you go
}

Prefer a one-shot file instead of an integration? Any list size can be run as a managed bulk job from your workspace: upload the file in the chat and the finished CSV is emailed to you with a download link in your History. Enterprise accounts can also hand us the file and we run it white glove.

Common recipes

Geo routing (phone to ZIP)

POST the caller’s number to /api/v1/resolve (default enrich mode) and read zip off the response, fast enough to sit inside live call routing. Route the call, then enrich the rest of the profile after connect if you want it.

Inbound call pre-qualification

Use resolve’s rtb mode: pass your filters (state, ZIP list, age range, homeowner, income floor) and get back a single pass boolean with no PII, built for bid or no-bid decisions on live calls.

Full data append (phone to PII++)

Batch up to 100 phones per call on /api/v1/enrich for name, address, demographics, email, IP, and source URL. For email or name-and-address keys instead of phones, use /api/v1/append-phone.

Realtime Resolution API · v1

Resolve a person in under 15ms

The Resolution API is the fast, single-identifier twin of the Enrichment API. Send any one identifier and get a small response back fast enough to sit inside an inbound-call routing loop. It is built for real-time bidding (RTB) pre-qualification, on-the-fly enrichment, and pixel resolution. Where /api/v1/enrich batches up to 100 phones against the warehouse, this endpoint answers a single lookup from a hot in-memory store, so it answers fast enough to sit inside a live call flow (typically a few hundred milliseconds end to end).

Under 15ms per lookup

One identifier in, a small answer out. Fast enough for the bid loop, not the batch job.

Any identifier, auto-detected

Phone, md5, sha1, or sha256. We pick the right index from the value’s shape.

Three response modes

Full enrich, a yes/no RTB pre-qual with no PII, or just the identity keys.

POST/api/v1/resolve

Authentication is identical to the Enrichment API. Send the same partner key in the X-API-Key header. A missing, invalid, inactive, or expired key returns 401.

The identifier

Send exactly one identifier as {"identifier": "..."}. We detect its kind automatically from its format, so you do not declare a type. For backward compatibility, the legacy {"phone": "..."} and {"sha256": "..."} fields are still accepted.

FormatDetected as
Phone10 digits, or 11 with a leading 1. Normalized to 10 digits.
md532 hex characters (md5 of a normalized email).
sha140 hex characters (sha1 of a normalized email).
sha25664 hex characters (sha256 of a normalized email).

Response modes

Choose what comes back with the optional mode field. The default is enrich. Any other value returns 400. On a miss every mode returns {"matched": false}. Null fields are always omitted, so a result only carries the attributes we actually hold.

mode: "enrich"default

Returns the populated PII and demographic fields for the resolved person, the same data points the batch /api/v1/enrich endpoint returns (including phones, email, IP, and source URL). Only non-null fields come back, so the example below shows a representative subset; the full set is in the table.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/resolve \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"identifier": "4155550142"}'

Request body

{
  "identifier": "4155550142"
}

Response

{
  "matched": true,
  "first_name": "Jordan",
  "last_name": "Avery",
  "address": "1240 Oak Ridge Dr",
  "city": "San Francisco",
  "state": "CA",
  "zip": "94110",
  "age": 47,
  "income_band": "L. $150,000-$174,999",
  "income_midpoint": 162500,
  "credit_range": "A. 800+",
  "credit_midpoint": 820,
  "homeowner": "Home Owner",
  "email": "jordan.avery@example.com"
}
FieldTypeDescription
matchedbooleanWhether the identifier resolved to a person.
first_namestringGiven name on the resolved identity.
last_namestringFamily name on the resolved identity.
addressstringStandardized street address.
citystringStandardized city.
statestringTwo-letter state code.
zipstringFive-digit ZIP code.
agenumberEstimated age.
genderstringGender on file.
marital_statusstringMarital status on file.
income_bandstringHousehold income band label, e.g. "L. $150,000-$174,999".
income_midpointnumberNumeric household income midpoint, in dollars.
credit_rangestringCredit range label, e.g. "A. 800+".
credit_midpointnumberNumeric credit-score midpoint.
homeownerstringHome ownership status: "Home Owner", "Renter", or "Probable Home Owner".
home_valuenumberEstimated home value, in dollars.
household_sizenumberNumber of people in the household.
birth_month_and_yearstringBirth month and year on file, e.g. "1977-04".
year_builtnumberYear the residence was built.
dncnumberDo Not Call state of the returned mobile. 0 means callable (the served mobile is DNC-clean by construction).
mobile_phonestringBest deliverable mobile number on the identity.
landline_phonestringBest landline on the identity.
emailstringBest email on the identity.
ipstringIP address associated with the identity.
source_urlstringOpt-in or lead source URL on the identity.
mode: "rtb"pre-qualification, no PII

For real-time bidding. Send the identifier plus pre-qual filters and get back only {"matched", "pass"}, never any PII. Filters can sit at the top level or under a filters object. Every filter you send must pass, and a filter referencing a field the profile does not carry fails (we will not assert a qualification we have no data for). A miss, or any failed filter, returns "pass": false.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/resolve \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "4155550142",
    "mode": "rtb",
    "state": "CA",
    "age_min": 35,
    "income_min": 100000,
    "homeowner": true
  }'

Request body

{
  "identifier": "4155550142",
  "mode": "rtb",
  "state": "CA",
  "age_min": 35,
  "income_min": 100000,
  "homeowner": true
}

Response

{
  "matched": true,
  "pass": true
}

Pre-qual filters (all optional)

FieldTypeDescription
zipstringProfile ZIP must match these five digits exactly.
statestringProfile state must match this two-letter code (case-insensitive).
age_minnumberProfile age must be at least this value.
age_maxnumberProfile age must be at most this value.
income_minnumberIncome midpoint must be at least this many dollars.
credit_minnumberCredit midpoint must be at least this value.
homeownerbooleanWhen true, the profile must be a (probable) homeowner.
mode: "identity"identity keys only

Returns only the identity keys for the resolved person (hashes, IP, source URL, and registration date), with no demographics. Useful for pixel resolution and identity stitching. Only non-null fields come back.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/resolve \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "5d41402abc4b2a76b9719d911017c592",
    "mode": "identity"
  }'

Request body

{
  "identifier": "5d41402abc4b2a76b9719d911017c592",
  "mode": "identity"
}

Response

{
  "matched": true,
  "md5": "5d41402abc4b2a76b9719d911017c592",
  "sha256": "9b2c…e4f1",
  "ip": "73.92.18.204",
  "source_url": "homeimprovementquotes.com",
  "registration_date": "2024-11-03"
}
FieldTypeDescription
matchedbooleanWhether the identifier resolved to a person.
md5stringmd5 of the normalized email on the identity.
sha256stringsha256 of the normalized email on the identity.
ipstringIP address associated with the identity.
source_urlstringOpt-in or lead source URL on the identity.
registration_datestringRegistration date on the source record.

Error codes

Errors return a JSON body shaped like {"error": "..."} with the matching HTTP status. Note there is no rate-cap status here: a miss is a normal 200 with "matched": false.

StatusMeaning
400Body is not valid JSON, the mode is unknown, or no valid identifier was supplied.
401Missing, invalid, inactive, or expired API key.
503The backend is not configured. Contact us if this persists.
Phone Append API

Append phones from an email or a mailing address

The reverse of the Enrichment API. Send up to 100 plaintext emails and/or up to 100 name-and-address records per call, and get back each person's phones (best number flagged), plus their name and postal address. Built for CRM phone-fill and outreach list preparation. Plan for 1 to 3 seconds per batch.

POST/api/v1/append-phone

Authentication is identical to the other endpoints: send your partner key in the X-API-Key header. Emails must be plaintext (we hash them server-side to hit the index; a pre-hashed value cannot be converted, so it will not match). Records match through the postal graph first; when only the record's email matches, a last-name gate keeps us from returning a different person, and a same-surname, different-first-name match comes back flagged at low confidence. We log match counts only: the emails you submit and the phones we return are never persisted.

cURL (emails)
curl -X POST https://www.talkdatatome.online/api/v1/append-phone \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_KEY" \
  -d '{"emails": ["jane.doe@example.com"]}'
Request body (records)
{
  "records": [
    {
      "first_name": "Jane",
      "last_name": "Doe",
      "address": "123 Main St",
      "city": "Tampa",
      "state": "FL",
      "zip": "33601",
      "email": "jane.doe@example.com"
    }
  ]
}
Response
{
  "requested": 1,
  "matched": 1,
  "results": [
    {
      "input": "jane.doe@example.com",
      "matched": true,
      "confidence": "medium",
      "first_name": "JANE",
      "last_name": "DOE",
      "address": "123 MAIN ST",
      "city": "TAMPA",
      "state": "FL",
      "zip": "33601",
      "phones": [
        { "phone": "8135550142", "type": "mobile", "rank": 1, "carrier": "T-MOBILE",
          "last_seen": "2026-04-18", "dnc": "callable", "best": true },
        { "phone": "8135550177", "type": "landline", "rank": 1, "carrier": null,
          "last_seen": "2025-11-02", "dnc": "callable", "best": false }
      ]
    }
  ]
}

Result fields

Email inputs come back in results (same order as submitted). Record inputs come back in record_results, each echoing the record you sent. A matched name and address also carries a demographics block (age, gender, income_band, credit_range, homeowner) from our own store; no id of any kind is needed. A miss is a normal 200 with "matched": false.

FieldTypeDescription
inputstringThe email you submitted, echoed back (record results echo the record fields instead).
matchedbooleanWhether the input resolved to a person with at least one phone.
confidencestring"medium" or "low". Low means treat with care (for example, the match may be another member of the same household).
first_namestringGiven name on the matched identity. Null on a miss.
last_namestringFamily name on the matched identity. Null on a miss.
addressstringStreet address on the matched identity.
citystringCity on the matched identity.
statestringTwo-letter state code.
zipstringZIP code.
phonesarrayEvery phone on the identity, ordered best-first. Empty on a miss.

Each entry in phones

FieldTypeDescription
phonestringTen-digit phone number.
typestring"mobile", "landline", or "voip".
ranknumberPosition within its tier (lower is fresher/stronger).
carrierstringCarrier name when known, else null.
last_seenstringDate this number was last confirmed active for the person.
dncstring"callable" or "DNC" (on the national Do Not Call registry).
bestbooleanTrue on exactly one phone per match: the single number to dial first.

Error codes

StatusMeaning
400Malformed request. Body is not valid JSON, no inputs given, or more than 100 emails / 100 records.
401Missing, invalid, inactive, or expired API key.
429Monthly matched-row cap reached for this key.
500Something failed on our side. Safe to retry shortly.

Every other endpoint on the same key

The key you already hold works on everything below: your pixel data, property and vehicle lists, your feeds, delivery and spend reporting, your suppression list, live record intake, revenue reporting, and the RoofRadar endpoints. Each one is in the OpenAPI spec with full request and response schemas; this page gives you the call, the response, and the things worth knowing before you build on it.

Website visitors: your pixel data, with email and phone

GETPOST/api/v1/website-visitors

The people your pixel resolved, as a file. GET counts them (free, nothing charged) and tells you how many carry an email, an address, a phone, and a cell phone, plus the top states and pages. Add any of the filters below as query parameters to size a selection. POST the same filters as JSON with a required max_records and get a secure download link to a CSV. Rows are never in the response body; a list belongs in a file you download.

The same export is one click in the app: open the Pixel menu, choose View your website funnel, then Export CSV. The API and the button produce the same file from the same selection.

cURL (count)

curl "https://www.talkdatatome.online/api/v1/website-visitors?seen_within_days=30&require_phone=true" \
  -H "X-API-Key: rk_your_key_here"

Response (count)

{
  "available": true,
  "selection": "seen in the last 30 days, with a phone",
  "people": 23,
  "with_email": 23,
  "with_address": 23,
  "did_more_than_browse": 11,
  "with_phone": 23,
  "with_cell_phone": 17,
  "phone_no_dnc_flag": 17,
  "phone_household_only": 0,
  "top_states": [{ "state": "FL", "people": 4 }, { "state": "CA", "people": 3 }],
  "top_pages": [{ "page": "/", "people": 18 }, { "page": "/signup", "people": 9 }],
  "most_recent_visit": "2026-09-08",
  "total_people_in_database": 73,
  "notes": "people is distinct people, not visits. Free, nothing charged. POST the same filters with max_records to get a file.",
  "phone_notes": "phone_no_dnc_flag counts numbers we hold no do-not-call flag for, which is not the same as verified callable. ..."
}

cURL (export)

curl -X POST https://www.talkdatatome.online/api/v1/website-visitors \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"seen_within_days": 30, "require_phone": true, "max_records": 5000}'

Response (export)

{
  "delivered": true,
  "rows": 23,
  "matched_before_cap": 23,
  "capped": false,
  "selection": "seen in the last 30 days, with a phone",
  "filename": "website_visitors_2026-09-08.csv",
  "download_url": "https://www.talkdatatome.online/api/downloads/...",
  "order_id": "2f1c0e4a-6b0e-4a1d-9c7e-3a2b1c0d9e8f",
  "note": "Secure link, valid 24 hours. One row per person."
}

Customers can export up to 200,000 people per call. The link is valid for 24 hours. When nobody matches, the response is {"delivered": false, "rows": 0} and nothing is charged. Before the pixel has resolved anyone, GET answers {"available": false} with a 200, not an error.

Filters (query parameters on GET, JSON fields on POST)

FieldTypeDescription
stateslistTwo-letter state codes. Comma-separated on GET, an array on POST.
citieslistCity names.
zipslistFive-digit ZIPs.
min_age / max_agenumberAge range.
min_hhi / max_hhinumberHousehold income range, in dollars.
page_containsstringOnly people who viewed a page whose path contains this text.
domainstringOnly people seen on this website, when the pixel runs on more than one.
seen_within_daysnumberOnly people seen in the last N days.
converted_onlybooleanOnly people who did more than browse (a form submit or other tracked conversion).
require_emailbooleanOnly people with an email on file.
require_phonebooleanOnly people with a phone on file.
max_recordsnumberPOST only, required. The most rows to deliver, up to 200,000.

Columns in the file, in order

FieldTypeDescription
first_namestringGiven name of the resolved person.
last_namestringFamily name.
emailstringBest email on the person.
addressstringStreet address.
citystringCity.
statestringTwo-letter state code.
zipstringZIP code.
agenumberAge, when known.
household_incomenumberHousehold income midpoint, in dollars.
first_seendateFirst visit the pixel recorded.
last_seendateMost recent visit.
visitsnumberDistinct visits.
pages_viewednumberPages viewed across all visits.
last_pagestringPath of the last page viewed.
convertedbooleanTrue when the person did more than browse.
websitestringThe site they were seen on.
phonestringTen-digit phone, when one is on file.
phone_typestringWhether the number is a cell or a landline.
phone_dncstringWhether the number carries a do-not-call flag.
phone_matchstringWhether the number is that person’s own line or their household’s.

A phone never ships as digits alone. phone_type says whether it is a cell, phone_dnc whether it carries a do-not-call flag, and phone_match whether it is that person’s own line or their household’s, so you can decide what is dialable rather than reading the whole file as a call list.

Property pull: homeowners by value, year built, and equity

POST/api/v1/property-pull

Homeowners in single-family homes, narrowed by an income floor, a home value window, a year-built window, and equity (the share of the home they own). Send estimate: true for a free funnel of counts; send max_records (1 to 100,000) instead to order the list, which is emailed, listed by GET /api/v1/pull, and downloadable below. Your saved suppression lists are already reflected in both.

cURL

# Free estimate
curl -X POST https://www.talkdatatome.online/api/v1/property-pull \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"estimate": true, "states": ["MI"], "min_home_value": 200000}'

# Order the list (same filters, max_records instead of estimate)
curl -X POST https://www.talkdatatome.online/api/v1/property-pull \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"states": ["MI"], "min_home_value": 200000, "min_equity_pct": 30, "max_records": 10000}'
# → 202 {"queued": true, "order_id": "...", "max_records": 10000, ...}

Response (estimate)

{
  "selection": "homeowners; single family; home value $200,000+; states: MI; one primary cell per person; DNC delivered as a flag column, not filtered",
  "people_in_geo": 9012568,
  "homeowner_base": 5690091,
  "with_qualifying_property": 1904376,
  "value_qualified": 1445245,
  "equity_qualified": 1445245,
  "unique_cells": 1130562,
  "dnc_flagged": 568995,
  "callable_non_dnc": 561567,
  "notes": "unique_cells = deliverable unique mobile numbers with DNC-flagged numbers INCLUDED (the file carries a DNC column). callable_non_dnc is the subset not on the DNC registry. Saved account suppression lists are already reflected."
}

Filters: states, zips (five digits or a three-digit prefix), cities, min_hhi, min_home_value, max_home_value, year_built_min, year_built_max, min_equity_pct, single_family_only, and cell_scope (primary, one cell per person, or all_cells). DNC is a flag, never a filter: numbers on the registry ship with a DNC column set to Y and are counted on both sides of the split, so your compliance decides. When an estimate cannot apply a filter it says so in ignored_filters with a warning, and the count reads high; do not quote it as if the filter had applied. An estimate is free of charge and never touches your balance or your monthly plan, but it is not unconditional: a key that is not linked to an account returns 403, and if your saved suppression lists cannot be read we return 503 rather than a count that quietly ignores them. Ordering needs list-pull access on the account.

The delivered file ends with Owner Status (Home Owner or Probable Home Owner) and Dwelling Type (Single Family and similar), so you can verify the homeowner and single-family selection on every row instead of taking it on trust. Both were appended after the existing columns, so nothing an importer already reads has moved.

Aged opt-in by vehicle: direct mail files

POST/api/v1/aged-optin

Aged opt-in auto insurance or auto warranty records (vertical), filtered by the vehicle on the form: vehicle_year_min and vehicle_year_max, vehicle_makes and vehicle_models to include or exclude, and exclude_heavy_duty, plus states, ZIPs, cities, and require_phone, require_email, require_vin. mode: "estimate" is free; mode: "order" with max_records (up to 200,000) queues the file, emailed and listed by GET /api/v1/pull.

cURL

curl -X POST https://www.talkdatatome.online/api/v1/aged-optin \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"mode": "estimate", "states": ["TX"], "vehicle_year_min": 2018, "exclude_heavy_duty": true}'
# Then order: {"mode": "order", ...same filters..., "max_records": 25000}  → 202

Response (estimate)

{
  "mode": "estimate",
  "selection": "auto insurance; states: TX; vehicle year 2018+; heavy duty excluded",
  "mailable_households": 48210,
  "with_phone": 41870,
  "with_email": 39955,
  "with_vin": 3104,
  "by_vehicle_year": [
    { "year": 2018, "households": 14120 },
    { "year": 2019, "households": 12904 }
  ],
  "notes": "mailable_households counts MAILABLE HOUSEHOLDS (name + street address + ZIP present, one row per household, newest form fill wins). This is the number that ships. ..."
}

Every count and every row is a mailable household: name, street address, and ZIP present, one row per household, newest form fill wins. That is smaller than a raw record count on purpose, because the count you are quoted has to be the count that ships. VIN coverage is low on this source, so quote with_vin before promising VINs. The file carries First_Name, Last_Name, Address, City, State, Zip, Vehicle_Make, Vehicle_Model, Vehicle_Year, and VIN, plus Phone and Email when include_contact is true; the phone is as supplied and not DNC-scrubbed, for record matching rather than dialing. For a second batch of the same selection, order again with offset set to the number already delivered.

Download a finished order

GET/api/v1/pull/{id}/download

The CSV for any completed order on your account, byte-for-byte the file that was emailed. Orders from /api/v1/pull, /api/v1/property-pull, and /api/v1/aged-optin all download here; the id comes from GET /api/v1/pull, which lists recent orders with a download_url once the file exists.

cURL
curl -L https://www.talkdatatome.online/api/v1/pull/975a3e48-d46a-4afa-80ed-a544b69f4ff5/download \
  -H "X-API-Key: rk_your_key_here" -o list.csv
# 409 {"error": "Order is running; no file yet. Poll GET /api/v1/pull."} until the file exists

Topic search: what you can target

GET/api/v1/topics/search

Roughly 50,000 targetable topics across seven universes: consumer and business in-market intent, keyword intent by funnel stage, inbound lead and caller verticals, live daily flows, and responder pools. Search with q, narrow with universe, category, or min_reach, page with limit and offset. The topic_id values are what POST /api/v1/pull accepts in topics. Metadata only: no PII, no records, nothing billed.

cURL

curl "https://www.talkdatatome.online/api/v1/topics/search?q=solar&limit=2" \
  -H "X-API-Key: rk_your_key_here"
# No parameters at all returns the universe summary

Response

{
  "query": "solar",
  "total": 122,
  "limit": 2,
  "offset": 0,
  "results": [
    { "topic_id": "inbound_leads_solar", "universe": "inbound_leads", "label": "Solar", "category": "Inbound", "description": "Opt-in lead records in this vertical.", "reach": 3740260 },
    { "topic_id": "responders_solar", "universe": "responders", "label": "Solar", "category": "Inbound", "description": "Known responders in this vertical.", "reach": 5036 }
  ],
  "universe_labels": {
    "intent_b2c": "Consumer in-market intent topics",
    "intent_b2b": "Business in-market intent topics",
    "keyword": "Keyword search intent, by funnel stage",
    "inbound_leads": "Opt-in lead records by vertical",
    "inbound_calls": "Inbound callers by vertical",
    "inbound_live": "Live daily lead flow by vertical",
    "responders": "Known responders by vertical"
  }
}

reach is how many people are addressable for that topic and is omitted for live daily flows, where no fixed total exists. Only the two intent universes can be pulled on demand; the others are delivered as managed feeds.

Your feeds, described in plain English

GET/api/v1/feeds

Every feed on the account with the same description every other surface renders: what it delivers, what the records are made of (the measured mix once enough runs exist), every active filter, the cap, the schedule, how repeats are handled, and where it goes with credentials removed. recent_changes is the last ten changes, each saying who made it. Read-only and free. The id is what the two endpoints below take.

cURL

curl https://www.talkdatatome.online/api/v1/feeds -H "X-API-Key: rk_your_key_here"

Response

{
  "count": 1,
  "feeds": [
    {
      "id": "996e114d-0fe5-414b-bb5d-9c075eb814b1",
      "name": "Final expense intent, daily",
      "active": true,
      "age_floor": null,
      "description": "Delivers audience people showing in-market web intent. 100% in-market web intent signals, no fill. ... Up to 5,000 records per run. Schedule: Daily at 7am ET. Repeat records are not filtered out. Delivered one record at a time to your webhook at ..., with your field mapping applied.",
      "characteristics": {
        "delivers": "Delivers audience people showing in-market web intent.",
        "composition": { "classes": [{ "class": "web_intent", "label": "in-market web intent signals" }], "single": true, "measured": null, "window": null, "text": "100% in-market web intent signals, no fill." },
        "filters": [],
        "cap": "Up to 5,000 records per run",
        "schedule": "Daily at 7am ET",
        "dedup": "Repeat records are not filtered out",
        "destination": "Delivered one record at a time to your webhook at ..., with your field mapping applied"
      },
      "recent_changes": [],
      "last_run_at": "2026-09-08T11:00:04.120000+00:00",
      "next_run_at": "2026-09-09T11:00:00+00:00",
      "manage_url": "/data-feeds/996e114d-0fe5-414b-bb5d-9c075eb814b1"
    }
  ]
}

Change a feed’s targeting: preview, then confirm

POST/api/v1/feeds/change

Two steps by contract. A call without confirm: true never changes anything: it returns the measured volume impact, from the feed’s own recent records, plus a confirm_token. Applying needs both confirm: true and that token, so a change cannot be one-shotted without somebody having seen the numbers. Tokens are good for about an hour; a stale one answers 409.

cURL

# Step 1: preview (nothing changes)
curl -X POST https://www.talkdatatome.online/api/v1/feeds/change \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"feed_id": "996e114d-0fe5-414b-bb5d-9c075eb814b1", "age_floor": 65}'

# Step 2: apply, with the token from the preview
curl -X POST https://www.talkdatatome.online/api/v1/feeds/change \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"feed_id": "996e114d-0fe5-414b-bb5d-9c075eb814b1", "age_floor": 65, "confirm": true, "confirm_token": "9f3a1c..."}'

Response

// Preview
{
  "applied": false,
  "feed": "Final expense intent, daily",
  "impact": { "current_floor": null, "proposed_floor": 65, ... },
  "already_satisfied": false,
  "confirm_token": "9f3a1c...",
  "next_step": "Show these numbers to the person you are working for and get their explicit yes. Then call this endpoint again with the same change plus confirm: true and this confirm_token."
}

// Applied
{
  "applied": true,
  "verified": true,
  "feed": "Final expense intent, daily",
  "age_floor_before": null,
  "age_floor_after": 65,
  "takes_effect": "next run",
  "description": "Delivers audience people showing in-market web intent. ... Filters: Age 65 and up. ..."
}

Only the fields you can edit on the feed page move here. Today that is the age floor: age_floor (18 to 99) or remove_age_floor: true. Endpoints, credentials, and new fields stay person-only. An applied change is read back from the saved row before the response says verified: true, lands in the feed’s change history as made by you, and takes effect on the next run.

Pause, resume, or cap a feed

POST/api/v1/feeds/control

action is pause, resume, or set_daily_cap (with daily_cap). The same module the campaign chat uses, so pausing here means exactly what pausing there means. The account comes from the key, never from the body; a feed on another account answers 404.

cURL

curl -X POST https://www.talkdatatome.online/api/v1/feeds/control \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"feed_id": "996e114d-0fe5-414b-bb5d-9c075eb814b1", "action": "set_daily_cap", "daily_cap": 2500}'
# action: "pause" | "resume" | "set_daily_cap"

Response

{
  "applied": true,
  "pending_approval": false,
  "kind": "data_feed",
  "feed_id": "996e114d-0fe5-414b-bb5d-9c075eb814b1",
  "daily_cap": 2500,
  "message": "Daily cap set to 2,500. Takes effect on the next run."
}

Going live is owner-gated. Resuming a paused data feed files an approval for the account owner instead of switching it on, and the response says so: "pending_approval": true, "applied": false. A daily cap above the plan ceiling is refused with 409 and nothing changes. A no-op (already paused, same cap) answers "applied": false with a message.

Audience delivery: what went out, per day

GET/api/v1/audience-delivery

Per ad platform audience, per day, how many records were delivered. ?days sets the rolling window (1 to 365, default 30), ?audience_id limits to one audience, and ?include_daily=false drops the per_day series when you only need the summary. Every number is records net new to that audience on that date: a person already on the list is never sent again, so repeats are never counted. Free, read-only, and never PII. The same module feeds the Overview page.

cURL

curl "https://www.talkdatatome.online/api/v1/audience-delivery?days=30&include_daily=false" \
  -H "X-API-Key: rk_your_key_here"

Response

{
  "window": { "days": 30, "from": "2026-08-10", "to": "2026-09-08", "today_is_partial": true },
  "audiences": [
    {
      "audience_id": "a74f0fc8288c9e6f",
      "name": "Homeowners 55+, Midwest",
      "platform": "Roku",
      "active": true,
      "sends_new_rows_daily": true,
      "cadence": "daily",
      "status": "delivering",
      "summary": "9,000 new records went out on Aug 16, then about 300 a day since.",
      "delivered_in_window": 15600,
      "days_in_window": 30,
      "days_with_delivery": 23,
      "average_per_delivery_day": 678,
      "average_per_calendar_day": 520,
      "best_day": { "date": "2026-08-16", "records": 9000 },
      "last_delivery": { "date": "2026-09-08", "records": 290 }
    }
  ],
  "totals": { "audiences": 1, "delivering": 1, "delivered_in_window": 15600 },
  "unavailable": false,
  "counts_what": "Every number is records NET NEW to that audience on that date. A person already on the list is not sent again, so repeats are never counted here."
}

Ad spend: the numbers your invoice is built from

GET/api/v1/ad-spend

What your connected ad accounts spent, from the same rows our billing reads, so an invoice and this endpoint cannot disagree. start and end are YYYY-MM-DD (default the last 30 days); group_by is day, account, campaign, or platform; and platform narrows to one. Dates are the ad account’s own reporting timezone, exactly as the platform reported them, so they match its dashboard across month boundaries.

cURL

curl "https://www.talkdatatome.online/api/v1/ad-spend?start=2026-08-01&end=2026-08-31&group_by=day" \
  -H "X-API-Key: rk_your_key_here"
# group_by: day | account | campaign | platform;  platform: meta | google_ads | vibe

Response

{
  "start": "2026-08-01",
  "end": "2026-08-31",
  "group_by": "day",
  "total_spend": 4812.4,
  "currency": "USD",
  "all_settled": false,
  "days": 31,
  "results": [
    { "key": "2026-08-01", "date": "2026-08-01", "spend": 161.2, "locked": 161.2, "settled": true, "impressions": 48120, "clicks": 391, "currency": "USD" },
    { "key": "2026-08-31", "date": "2026-08-31", "spend": 149.9, "locked": null, "settled": false, "impressions": 45010, "clicks": 355, "currency": "USD" }
  ]
}

Two fields to read before reconciling

FieldTypeDescription
spendnumberDollars the platform reports for the group, two decimals. May still drift on unsettled days.
settledbooleanFalse while the platform is still restating that day. A group is settled only if every day in it is. Unsettled spend is real but not final, and not what gets billed.
lockednumber or nullThe value frozen when a billing period closed. Never moves once set, because that is the number the invoice was built from. Null until a period has closed.
impressions / clicksnumberAs reported by the platform.
currencystringComma-joined when an account reports in more than one. Totals across currencies are never summed silently.
all_settledbooleanTop level. Every day in the range is final and safe to reconcile against.

Suppressions: your do-not-contact list

GETPOST/api/v1/suppressions

Grow the account’s do-not-contact list from your own systems. POST phones (up to 50,000 per call, ten-digit US numbers, a leading 1 accepted) and they are excluded from every pull on the account from the next one on, and blocked on the realtime API too, with no other step. Additive only: nothing here ever removes a number, and repeats are ignored, so re-posting is safe. GET returns the counts, split by how the numbers arrived.

cURL
curl -X POST https://www.talkdatatome.online/api/v1/suppressions \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"phones": ["4155550142", "1 (212) 555-0188"]}'
# → {"ok": true, "received": 2, "added": 2, "total_phones": 214}

curl https://www.talkdatatome.online/api/v1/suppressions -H "X-API-Key: rk_your_key_here"
# → {"phones_uploaded": 12480, "phones_via_api": 214, "total": 12694}

Your database, in market: count who is shopping, then take the records

GETPOST/api/v1/activation/datasets
POST/api/v1/activation/datasets/{id}/in-market

Upload your own database once, then ask who in it is in market right now and take those records, all from code. POST /api/v1/activation/datasets creates a database from a records array (up to 50,000 per call) or a CSV (as {"csv": "..."}, a text/csv body, or a multipart file, up to 1,000,000 rows), and matches every row to a person. Identity fields are detected by name; every other column is yours and is kept with the row, so an agency sends one file with a client column on every row. GET lists your databases with the columns each can be filtered on.

POST .../{id}/in-market answers "how many in my database are in market for roofing where client is acme". topic is free text resolved against the topic list (the response says what it matched), or name exact topic_ids. Leave the topic off for everyone in market for anything, with every topic each person touched on the row. where narrows to your own columns: {"client": "acme"} or a list of values. A column the database does not hold answers 422 and names the ones it does, so a misspelled tag is never a quiet zero.

cURL

# 1. Create the database once (your own columns such as client are kept with each row)
curl -X POST https://www.talkdatatome.online/api/v1/activation/datasets \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Q3 clients", "records": [
        {"email": "jane.doe@example.com", "phone": "4155550142", "first_name": "Jane", "last_name": "Doe", "zip": "94110", "client": "acme"},
        {"email": "sam@example.com", "zip": "10001", "client": "globex"}
      ]}'

# 2. Count who is in market for roofing where client is acme (free)
curl -X POST https://www.talkdatatome.online/api/v1/activation/datasets/<dataset_id>/in-market \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"topic": "roofing", "where": {"client": "acme"}, "recency_days": 7}'

# 3. Take those records (billed per record). Leave topic off for anyone in market for anything.
curl -X POST https://www.talkdatatome.online/api/v1/activation/datasets/<dataset_id>/in-market \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"mode": "pull", "topic": "roofing", "where": {"client": "acme"}, "recency_days": 7, "max_records": 5000}'

Response

// Estimate
{
  "mode": "estimate",
  "database": "Q3 clients",
  "dataset_id": "0d3b6a1e-2c4f-4e8a-9b7d-5f6e7a8b9c0d",
  "rows": 48210,
  "matched_records": 41007,
  "topic": "roofing",
  "topics_matched": ["Roofing", "Roof Repair", "Roofing Contractors"],
  "recency_days": 7,
  "in_market": 312,
  "filtered_to": "client is acme",
  "breakdown": { "column": "client", "values": [{ "value": "acme", "in_market": 312 }] },
  "freshness": { "signal_date": "2026-09-22", "expected_lag_days": 2, "note": "In-market signals are a daily snapshot and normally land about 2 days behind. ..." },
  "next_step": "Pull them with mode \"pull\" and the same topic and where. 312 is the count that ships, before your account's caps."
}

// Pull
{
  "mode": "pull",
  "rows": 312,
  "columns": ["first_name", "last_name", "zip", "phone", "email", "current_mobile", "in_market_topics", "client", "List_ID"],
  "records": [
    { "first_name": "Jane", "last_name": "Doe", "zip": "94110", "phone": "4155550142", "email": "jane.doe@example.com",
      "current_mobile": "4155550142", "in_market_topics": "Roofing | Roof Repair", "client": "acme", "List_ID": "INMARKET-0D3B-20260924-A1B2C3D4" }
  ],
  "order_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "list_id": "INMARKET-0D3B-20260924-A1B2C3D4",
  "download_url": "https://www.talkdatatome.online/api/v1/pull/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/download",
  "charged_records": 312,
  "freshness": { "signal_date": "2026-09-22", "expected_lag_days": 2, "note": "..." }
}

mode: estimate (the default) is free and returns the count plus a breakdown by your column: the first column named in where, or any column you name in breakdown_by (leave where off and set "breakdown_by": "client" for every client at once). mode: pull returns the records: the identifiers you sent, current_mobile (the best mobile we hold), the topics each person is shopping, your columns, and List_ID on every row. It is billed per record through the same meter as any pull on the account and refused with a 402 up front when the plan or the prepaid balance cannot cover max_records. The file is also stored, so it lists under GET /api/v1/pull and downloads from GET /api/v1/pull/{order_id}/download; send format: csv to get the file straight back (the list id and the signal date then ride in the X-List-Id and X-Signal-Date headers). Nothing is charged for a count; a pull is charged only for the records it returns, so an empty result costs nothing. Every response carries freshness.signal_date: in-market signals are a daily snapshot and normally land about two days behind, so read a count as this week rather than this morning, and recency_days (1 to 30, default 3) counts back from that date. The same four steps are available to a connected assistant as the list_my_databases, create_my_database, estimate_my_database_in_market and pull_my_database_in_market tools, and signed in on the Database Activation page, step 3.

Live record intake for a connected database

POST/api/v1/activation/datasets/{id}/records

Point a website form, a CRM automation, or a Zapier webhook at this URL and every new sign-up lands in the connected database as it happens, matched the same way the batch loaders match. The body is one record, an array, or {"records": [...]}, up to 500 per call. Field names are detected the way CSV headers are ("Email Address", "phone_number", "First Name", zip or postal). Each record needs an email or a phone. Repeats and webhook retries are deduplicated, never double-inserted. The dataset id is on the Database Activation page.

cURL

# Dry run first: checks the key, the database, and the field names, stores nothing
curl -X POST "https://www.talkdatatome.online/api/v1/activation/datasets/<dataset_id>/records?dry_run=1" \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"Email Address": "jane.doe@example.com", "phone_number": "4155550142", "First Name": "Jane"}'

# Then the real call: one record, an array, or {"records": [...]}, up to 500 per call
curl -X POST https://www.talkdatatome.online/api/v1/activation/datasets/<dataset_id>/records \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"email": "jane.doe@example.com", "zip": "94110"}, {"phone": "2125550188"}]}'

Response

// Dry run
{
  "ok": true,
  "dry_run": true,
  "dataset_id": "0d3b6a1e-2c4f-4e8a-9b7d-5f6e7a8b9c0d",
  "dataset_name": "Newsletter sign-ups",
  "received": 1,
  "would_insert": 1,
  "unusable": 0,
  "records": [{ "index": 0, "usable": true, "matched_on": "email", "read": { "email": "jane.doe@example.com", "phone": "4155550142", "first_name": "Jane" }, "ignored": [], "dropped": [] }],
  "note": "Your key and this database check out. Nothing was stored. Send the same call without dry_run to load it for real."
}

// Live
{
  "ok": true,
  "dataset_id": "0d3b6a1e-2c4f-4e8a-9b7d-5f6e7a8b9c0d",
  "received": 2,
  "inserted": 2,
  "matched": 1,
  "duplicates": 0,
  "invalid": 0,
  "dataset": { "rows": 1204, "matched": 871 }
}

Test mode: add ?dry_run=1 (or "dry_run": true in the body) and we check the key, the database, and every field name we can read out of your payload, then store nothing. It is the same code path right up to the insert, so a green dry run means the real call will work; the Test button on the Database Activation page is this call. Form tools that cannot set headers may pass the key as ?key= instead. An unknown dataset answers 404.

Revenue reports: what a delivered list earned

GETPOST/api/v1/revenue

For buyers on a revenue share. Every list we deliver carries a list_id on the file or in the payload, like FINALEXPEN-996E-20260824-222CFF7A. Post what you owe TDTM against it as a total for a period (reports) or one sale at a time (transactions). Both price identically; sales are better evidence. Up to 500 items per call. GET reads back what you reported, and ?mode=lists returns the recent deliveries you can report against.

cURL

# A total for one list
curl -X POST https://www.talkdatatome.online/api/v1/revenue \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"reports": [{"list_id": "FINALEXPEN-996E-20260824-222CFF7A", "revenue": 4120.00}]}'

# Or every sale, one at a time
curl -X POST https://www.talkdatatome.online/api/v1/revenue \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"transactions": [{"list_id": "FINALEXPEN-996E-20260824-222CFF7A", "revenue": 62.50, "occurred_on": "2026-09-14", "record_ref": "lead-88"}]}'

# Name the person who converted and the sale ties to the record we sent.
# Plaintext or hashed, optional, and it never changes what you are paid.
curl -X POST https://www.talkdatatome.online/api/v1/revenue \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"transactions": [{"list_id": "FINALEXPEN-996E-20260824-222CFF7A", "revenue": 62.50, "occurred_on": "2026-09-14", "email": "jane.doe@example.com"}]}'

# Lost the file? The list ids you can report against:
curl "https://www.talkdatatome.online/api/v1/revenue?mode=lists" -H "X-API-Key: rk_your_key_here"

Response

{
  "ok": true,
  "accepted": 1,
  "rejected": 0,
  "matched_to_a_delivery": 1,
  "transactions": 0,
  "period_totals": 1,
  "results": [
    {
      "index": 0,
      "status": "stored",
      "id": "7c2d...",
      "list_id": "FINALEXPEN-996E-20260824-222CFF7A",
      "feed_code": "FINALEXPEN-996E",
      "kind": "period",
      "occurred_on": null,
      "match": "matched_run",
      "revenue_cents": 412000,
      "currency": "USD",
      "revision": 1,
      "replaced_revenue_cents": null,
      "warnings": []
    }
  ]
}

// A sale that named the person who converted carries identity_match, which is
// honest about what was found: matched, matched_other_delivery, no_match, or
// not_checked. It never affects the money.
{
  "ok": true,
  "accepted": 1,
  "transactions": 1,
  "sales_tied_to_a_record": 1,
  "results": [
    { "index": 0, "status": "stored", "match": "matched_run", "identity_match": "matched", "revenue_cents": 6250 }
  ]
}

// GET ?mode=lists
{
  "ok": true,
  "lists": [
    { "list_id": "FINALEXPEN-996E-20260824-222CFF7A", "feed_code": "FINALEXPEN-996E", "feed": "Final expense intent, daily", "delivered_at": "2026-08-24T23:11:17.827542+00:00", "records": 10, "status": "completed" }
  ]
}

Three rules. Your figure is never lost: a report we cannot tie to a delivery is stored with "match": "unmatched" and returned with an explanation, not answered with a 400. Nothing is invented: unmatched money is shown for exactly what it is. Re-posting is safe: a report is keyed on what it is about (list, period, your own external_id), so a second post updates the row, bumps revision, and keeps the figure it replaced. Send both sales and the total they add up to and you are not counted twice; the sales win. Rows that fail validation are answered per row in results (a missing list_id, for example); the batch as a whole is 400 only when nothing in it could be stored.

Fields on each item

FieldTypeDescription
list_idstringThe id on the file or payload we sent you. One list id is one delivery. Required unless feed_code is given.
revenuenumberGross revenue on those records, in dollars. Send revenue_cents instead to work in whole cents.
period_start / period_enddateYYYY-MM-DD. Optional; the delivery date is already known from the list_id.
feed_codestringOptional, alongside the list_id. It never replaces it: a report with a feed_code and no list_id is rejected, because revenue has to tie to the exact delivery it came from.
external_idstringYour own reference, such as an invoice id. Re-sending the same one updates the report; a different one is a new report.
occurred_ondateTransactions only, required. The day the sale happened.
record_refstringTransactions only. Your reference for the record that earned. Never used to price.
emailstringTransactions only, optional. The email of the person who converted, or an md5, sha1 or sha256 of it. Ties the sale to the exact record we delivered, which is what sharpens the audiences we build for you. Hashed on arrival, never stored as plaintext, and never used to price.
phonestringTransactions only, optional. The same thing as a phone number, any formatting. Also accepted as email_sha256, email_md5, phone_sha256 and phone_md5 if you would rather be explicit. Never used to price.
records_soldnumberHow many records converted. Explains the figure, never prices anything.
currencystringISO code, default USD.

RoofRadar: lists and lookups for your territory

Three endpoints for RoofRadar accounts, on the same key. All three need a RoofRadar plan on the account and answer 402 without one. ZIPs have to be inside the plan’s territory (403 otherwise, with the ZIPs that were outside listed). Records count against the plan on the first download of a list, or on a matched lookup; a miss is free.

POSTGET/api/v1/roofradar/pull

POST ZIPs (up to 1,000), a required max_records (1 to 100,000), and optional filters: built_on_or_before, built_on_or_after, min_home_value, max_home_value, min_income, min_owner_age, max_owner_age, phone (mobile, any, or none), and require_email. mode is homeowners (default) or intent (an add-on); columns is full or dialer. The list builds in the background and is emailed; GET lists your recent lists with download links and what is left on the plan.

cURL

curl -X POST https://www.talkdatatome.online/api/v1/roofradar/pull \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"zips": ["49503", "49504"], "max_records": 500, "filters": {"built_on_or_before": 2005, "phone": "mobile"}}'
# → 202 {"queued": true, "mode": "homeowners", "max_records": 500, "zips": 2, "warnings": [], "note": "Building now. ..."}

curl https://www.talkdatatome.online/api/v1/roofradar/pull -H "X-API-Key: rk_your_key_here"

Response (GET)

{
  "lists": [
    {
      "id": "4b9eee20-5d9c-45d7-a8a3-308be83c7905",
      "created_at": "2026-09-08T14:02:11.000000+00:00",
      "completed_at": "2026-09-08T14:03:40.000000+00:00",
      "status": "completed",
      "records": 500,
      "mode": "homeowners",
      "summary": "built 2005 or earlier; mobile phone required",
      "downloaded": false,
      "download_url": "/api/v1/roofradar/lists/4b9eee20-5d9c-45d7-a8a3-308be83c7905/download",
      "ad_platform_url": null,
      "emailed_only": false,
      "error": null
    }
  ],
  "records_remaining": 9500,
  "period_end": "2026-10-01T00:00:00+00:00"
}
POST/api/v1/roofradar/resolve

One home, its roofing facts, its owner. Send {"address", "zip"} or {"phone"}. A match costs one record; up to 60 lookups a minute per key. roof_cover is a label only when the code is known, otherwise null with the raw code in roof_cover_code, never a guess. Phones on your do-not-contact list are withheld and suppressed_contact is set.

cURL

curl -X POST https://www.talkdatatome.online/api/v1/roofradar/resolve \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"address": "745 Monroe Ave NW", "zip": "49503"}'
# or  -d '{"phone": "6165550100"}'

Response

{
  "matched": true,
  "matched_by": "address",
  "billed": true,
  "records_remaining": 9499,
  "address": "745 MONROE AVE NW",
  "city": "GRAND RAPIDS",
  "state": "MI",
  "zip": "49503",
  "county": "KENT",
  "owner_occupied": true,
  "year_built": 1998,
  "roof_age_years": 28,
  "roof_cover": "Asphalt shingle",
  "roof_cover_code": 3,
  "living_sqft": 1840,
  "home_value": 312000,
  "owner": { "first_name": "Jordan", "last_name": "Avery", "mobile_phone": "6165550100", "landline_phone": null, "email": "jordan.avery@example.com", "age": 51, "household_income": 92500 }
}
GET/api/v1/roofradar/lists/{id}/download

The CSV for a finished list. The first download meters the records against the plan; later downloads are free. When the remaining allotment is short the download is refused with 402 rather than partially billed. ?format=hashed serves the ad platform file (SHA256 email and phone), only after the list itself has been downloaded once.

cURL
curl -L https://www.talkdatatome.online/api/v1/roofradar/lists/<list_id>/download \
  -H "X-API-Key: rk_your_key_here" -o roofing_list.csv
# Add ?format=hashed for the ad platform file, once the list itself has been downloaded

Identify (deprecated)

POST/api/v1/identify

An alias of POST /api/v1/enrich with records: the same name-gated waterfall over phone, email, and name plus address, returning the identical shape. It stays only so integrations built on it keep working. New integrations should call /api/v1/enrich (documented above); for a single real-time phone, use /api/v1/resolve with verify.

Use this instead
# Deprecated. Send the same body to /api/v1/enrich instead:
curl -X POST https://www.talkdatatome.online/api/v1/enrich \
  -H "X-API-Key: rk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"records": [{"phone": "4045550142", "first_name": "Jane", "last_name": "Doe", "zip": "30301"}]}'

Status codes across these endpoints

Errors return a JSON body shaped like {"error": "..."} with the matching HTTP status, and the message says what to change. These are the ones you will meet.

StatusMeaning
400Something in the request is missing or malformed. The body says what: a required max_records ("max_records is required: 1 to 100,000"), an unknown feeds/control action ("action must be one of pause, resume, set_daily_cap"), an aged-optin mode that is not estimate or order, an empty phones array on suppressions, or a revenue batch where every item was rejected (per-item reasons in results).
401No key, or a key that is invalid, inactive, or expired. With no header at all: {"error": "Missing or malformed API key. Send it in the X-API-Key header (starts with rk_)."}
402Money or plan. A RoofRadar endpoint without a RoofRadar plan on the account, an order above the records left on the plan this month, or an empty prepaid balance (error: "wallet_empty").
403The key is not linked to an account, list ordering is not enabled on it (estimates still work), or a ZIP is outside your RoofRadar territory.
404The id is not on this account: a feed, an order, an audience, a dataset, or a RoofRadar list.
409Not yet, or not like that. A pull or list with no file yet (poll first), a stale confirm_token on feeds/change, a daily cap above the plan ceiling, or a dataset mid-load.
413Too many in one call: over 500 activation records, over 500 revenue items, or a suppression list that would pass 500,000 numbers (upload a file instead).
422Activation only: no record carried an email or a phone, or the dataset is at its row limit.
429RoofRadar resolve only: more than 60 lookups in a minute.

Ready to start enriching?

Create your key on the Developers page and make your first call in minutes. Need a higher cap or volume pricing? Talk to us.