{"openapi":"3.1.0","info":{"title":"TDTM API","version":"1.4.0","summary":"Identity resolution, batch enrichment, and audience delivery.","description":"## Overview\n\nWhat you can do with an API key:\n\n1. **Resolve** a single identifier in real time, fast enough to sit inside a call-routing or bidding loop.\n2. **Enrich** a batch of phones, emails, IPs, or partial records into full profiles.\n3. **Export your website visitors**: the people your pixel resolved, with email and phone, as a CSV (`/api/v1/website-visitors`).\n4. **Pull** a filtered list or audience and collect it as CSV: by ZIP, by topic, by property, or by vehicle.\n5. **Run your feeds**: read what each feed delivers, preview and confirm a targeting change, pause, resume, or set a daily cap.\n6. **Read your own numbers**: usage, what went out to each ad platform audience, and what your connected ad accounts spent.\n7. **Keep your account clean**: grow your do-not-contact list, stream sign-ups into a connected database, and report revenue on a rev share.\n\n## Authentication\n\nSend your key in the `X-API-Key` header (it starts with `rk_`). `X-Relay-API-Key` is accepted as a legacy alias. Keys are issued from the Developers page, under the API tab in the top bar. Bearer tokens and OAuth are not supported.\n\n## Billing\n\nYou are billed on **matched** records, never on requests or misses. Field groups are free:\nasking for demographics, property, and vehicles on the same record still counts as one record.\nRecords on your account suppression list return `suppressed: true` and are neither billed nor\ncounted against your cap.\n\n## What a match means\n\nA 200 response means the request was accepted, not that every input matched. Check the\n`matched` count and the per-row `matched` flag. Where an identifier is an association rather\nthan an identification (notably IP), the response reports how many people sit behind it and a\nconfidence band, and declines rather than guessing.","contact":{"name":"TDTM support","url":"https://www.talkdatatome.online/support"},"license":{"name":"Proprietary. Use governed by the TDTM terms of service","url":"https://www.talkdatatome.online/terms"}},"servers":[{"url":"https://www.talkdatatome.online","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Realtime","description":"Single-identifier lookups on the hot path."},{"name":"Enrichment","description":"Batch append against the identity graph."},{"name":"Lists","description":"Filtered list and audience pulls, delivered as CSV."},{"name":"Discovery","description":"Find what you can target before you buy it."},{"name":"Pixel","description":"The people your website pixel resolved, counted and exported."},{"name":"Feeds","description":"Read, adjust, pause, and resume the feeds that deliver to you."},{"name":"Account","description":"Usage, delivery and spend reporting, and your do-not-contact list."},{"name":"Activation","description":"Stream new records into a connected database as they arrive."},{"name":"Rev share","description":"Report what a delivered list earned, by list id."},{"name":"RoofRadar","description":"Roofing lists and lookups. Needs a RoofRadar plan on the account."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Send your key in the `X-API-Key` header (it starts with `rk_`). `X-Relay-API-Key` is accepted as a legacy alias. Keys are issued from the Developers page, under the API tab in the top bar. Bearer tokens and OAuth are not supported."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable failure reason."}},"required":["error"]},"PhoneResult":{"type":"object","description":"One row per submitted phone, in submitted order.","properties":{"phone":{"type":"string","description":"Echoed back exactly as you sent it."},"matched":{"type":"boolean"},"suppressed":{"type":"boolean","description":"Present and true when the number is on your account suppression list. No data is returned and the record is not billed."},"matched_by":{"type":"string","enum":["phone"],"description":"Present on a match. The phone number resolved the person."},"confidence":{"type":"string","enum":["high","medium","low"],"description":"How strongly the returned person is tied to this number. A verified pairing is always \"high\". Otherwise \"high\": exactly one person on the number, it is their primary line, and the pairing was seen within the last 12 months. \"medium\": 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\": a third or later line, seen more than 24 months ago or never dated, or three or more people on the number. Also present on a row declined by `min_confidence`, so you can see the band you turned away."},"verified":{"type":"boolean","description":"True 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."},"phone_people":{"type":"integer","description":"How many distinct people our graph has on this number, the verified person included."},"reason":{"type":"string","enum":["below_min_confidence"],"description":"Present when you sent `min_confidence` and the match fell below it. The row is not a match and is not billed."},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string","description":"Survived mailing address (golden profile preferred over the raw source row)."},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"age":{"type":"integer"},"gender":{"type":"string"},"marital_status":{"type":"string"},"homeowner":{"type":"string","description":"e.g. \"Home Owner\", \"Renter\". Absent when unknown."},"household_income":{"type":"integer","description":"Household income midpoint in USD."},"mobile_phone":{"type":"string"},"landline_phone":{"type":"string"},"email":{"type":"string","description":"The person's primary (rank-1) email, which may differ from the address you submitted."},"ip_address":{"type":"string"},"source_url":{"type":"string","description":"Where our graph last observed this person."},"demographics":{"type":["object","null"],"description":"Curated consumer attributes (~160 fields). Sensitive classes (ethnicity, religion, language, political) are never returned."},"property":{"type":["object","null"],"description":"Property record for the person's address."},"financial":{"type":["object","null"],"description":"Financial attributes."},"vehicles":{"type":"array","items":{"type":"object"},"description":"Up to 5 vehicles, VIN included. Requested as field group \"auto\"."},"executive_roles":{"type":"array","items":{"type":"object"},"description":"Up to 3 business roles. The B2B executive-at-home overlay, present when the person is a linked business executive. Requested as field group \"executive\"."},"geo":{"type":["object","null"],"description":"Territory + precision block: lat/long, county, DMA, MSA, census geography, carrier route."},"contacts":{"type":"object","description":"ALL known phones and emails for the person, not just the best one.","properties":{"phones":{"type":"array","items":{"type":"object"}},"emails":{"type":"array","items":{"type":"object"}}}},"purchase_behavior":{"type":["object","null"],"description":"Recent purchase aggregates by category: dollars, orders, items."},"signals":{"type":"object","description":"In-market intent signals with topic, frequency, recency.","properties":{"b2c":{"type":"array","items":{"type":"object"}},"b2b":{"type":"array","items":{"type":"object"}}}},"behavior":{"type":"array","items":{"type":"object"},"description":"IAB behavioral categories with recency."}},"required":["phone","matched"]},"EmailResult":{"type":"object","description":"One row per submitted email, in submitted order, including entries we could not parse.","properties":{"seed_email":{"type":"string","description":"Echoed back exactly as you sent it."},"matched":{"type":"boolean"},"matched_by":{"type":"string","enum":["email","email_md5","email_sha1","email_sha256"],"description":"Which form of the address resolved the person."},"reason":{"type":"string","enum":["invalid_email"],"description":"Present when the input was neither an email address nor a recognized hash."},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string","description":"Survived mailing address (golden profile preferred over the raw source row)."},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"age":{"type":"integer"},"gender":{"type":"string"},"marital_status":{"type":"string"},"homeowner":{"type":"string","description":"e.g. \"Home Owner\", \"Renter\". Absent when unknown."},"household_income":{"type":"integer","description":"Household income midpoint in USD."},"mobile_phone":{"type":"string"},"landline_phone":{"type":"string"},"email":{"type":"string","description":"The person's primary (rank-1) email, which may differ from the address you submitted."},"ip_address":{"type":"string"},"source_url":{"type":"string","description":"Where our graph last observed this person."},"demographics":{"type":["object","null"],"description":"Curated consumer attributes (~160 fields). Sensitive classes (ethnicity, religion, language, political) are never returned."},"property":{"type":["object","null"],"description":"Property record for the person's address."},"financial":{"type":["object","null"],"description":"Financial attributes."},"vehicles":{"type":"array","items":{"type":"object"},"description":"Up to 5 vehicles, VIN included. Requested as field group \"auto\"."},"executive_roles":{"type":"array","items":{"type":"object"},"description":"Up to 3 business roles. The B2B executive-at-home overlay, present when the person is a linked business executive. Requested as field group \"executive\"."},"geo":{"type":["object","null"],"description":"Territory + precision block: lat/long, county, DMA, MSA, census geography, carrier route."},"contacts":{"type":"object","description":"ALL known phones and emails for the person, not just the best one.","properties":{"phones":{"type":"array","items":{"type":"object"}},"emails":{"type":"array","items":{"type":"object"}}}},"purchase_behavior":{"type":["object","null"],"description":"Recent purchase aggregates by category: dollars, orders, items."},"signals":{"type":"object","description":"In-market intent signals with topic, frequency, recency.","properties":{"b2c":{"type":"array","items":{"type":"object"}},"b2b":{"type":"array","items":{"type":"object"}}}},"behavior":{"type":"array","items":{"type":"object"},"description":"IAB behavioral categories with recency."}},"required":["seed_email","matched"]},"IpResult":{"type":"object","description":"One row per submitted IP, in submitted order. An IP is an association, not an identification: we return a person only when the address maps to a household-sized set of people, and always report how many.","properties":{"seed_ip":{"type":"string"},"matched":{"type":"boolean"},"matched_by":{"type":"string","enum":["ip"]},"ip_identities":{"type":"integer","description":"How many distinct people our graph has observed behind this address."},"confidence":{"type":"string","enum":["high","household"],"description":"\"high\" when exactly one person sits behind the IP; \"household\" when more than one does."},"reason":{"type":"string","enum":["invalid_ip","shared_ip","ip_lookup_unavailable"],"description":"\"shared_ip\" means the address is a carrier, proxy, or datacenter endpoint with too many people behind it to identify anyone; we decline rather than return a stranger. \"ip_lookup_unavailable\" means this seed timed out while the rest of your request succeeded."},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string","description":"Survived mailing address (golden profile preferred over the raw source row)."},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"age":{"type":"integer"},"gender":{"type":"string"},"marital_status":{"type":"string"},"homeowner":{"type":"string","description":"e.g. \"Home Owner\", \"Renter\". Absent when unknown."},"household_income":{"type":"integer","description":"Household income midpoint in USD."},"mobile_phone":{"type":"string"},"landline_phone":{"type":"string"},"email":{"type":"string","description":"The person's primary (rank-1) email, which may differ from the address you submitted."},"ip_address":{"type":"string"},"source_url":{"type":"string","description":"Where our graph last observed this person."},"demographics":{"type":["object","null"],"description":"Curated consumer attributes (~160 fields). Sensitive classes (ethnicity, religion, language, political) are never returned."},"property":{"type":["object","null"],"description":"Property record for the person's address."},"financial":{"type":["object","null"],"description":"Financial attributes."},"vehicles":{"type":"array","items":{"type":"object"},"description":"Up to 5 vehicles, VIN included. Requested as field group \"auto\"."},"executive_roles":{"type":"array","items":{"type":"object"},"description":"Up to 3 business roles. The B2B executive-at-home overlay, present when the person is a linked business executive. Requested as field group \"executive\"."},"geo":{"type":["object","null"],"description":"Territory + precision block: lat/long, county, DMA, MSA, census geography, carrier route."},"contacts":{"type":"object","description":"ALL known phones and emails for the person, not just the best one.","properties":{"phones":{"type":"array","items":{"type":"object"}},"emails":{"type":"array","items":{"type":"object"}}}},"purchase_behavior":{"type":["object","null"],"description":"Recent purchase aggregates by category: dollars, orders, items."},"signals":{"type":"object","description":"In-market intent signals with topic, frequency, recency.","properties":{"b2c":{"type":"array","items":{"type":"object"}},"b2b":{"type":"array","items":{"type":"object"}}}},"behavior":{"type":"array","items":{"type":"object"},"description":"IAB behavioral categories with recency."}},"required":["seed_ip","matched"]},"Topic":{"type":"object","description":"One targetable topic. Never carries a data source: the catalog stores what the data IS, never who it came from.","properties":{"topic_id":{"type":"string","description":"Pass this to a pull or audience build."},"universe":{"type":"string","enum":["intent_b2c","intent_b2b","keyword","inbound_leads","inbound_calls","inbound_live","responders"]},"label":{"type":"string"},"category":{"type":"string"},"subcategory":{"type":"string"},"description":{"type":"string"},"reach":{"type":"integer","description":"People addressable. OMITTED for live daily flows, where no fixed total exists."},"last_seen":{"type":"string","description":"Most recent date this topic had activity."}},"required":["topic_id","universe","label"]},"RecordResult":{"type":"object","properties":{"matched":{"type":"boolean"},"matched_by":{"type":"string","description":"Which identifier resolved the person (e.g. \"phone\", \"email\", \"name_address\")."},"confidence":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"homeowner":{"type":"string"},"is_homeowner":{"type":"boolean"},"line_type":{"type":"string","description":"e.g. \"mobile\", \"landline\"."},"dnc":{"type":["integer","null"],"description":"Do-not-call flag on the returned phone."},"age":{"type":["integer","null"]},"gender":{"type":["string","null"],"description":"\"M\" or \"F\" where known."},"income_band":{"type":["string","null"],"description":"Household income band text, e.g. \"K. $100,000-$149,999\"."},"income_midpoint":{"type":["number","null"]},"credit_range":{"type":["string","null"],"description":"Credit range text, e.g. \"C. 700-749\"."}},"description":"Name-gated waterfall result: we confirm or decline, never substituting a different person. A name plus address is enough on its own; no id of any kind is needed to get the demographics back.","required":["matched"]}}},"paths":{"/api/v1/resolve":{"post":{"tags":["Realtime"],"operationId":"resolve","summary":"Resolve one identifier in real time","description":"Give any single identifier, get a small response fast enough to sit inside an inbound-call\nrouting or bidding loop. This is the trimmed twin of `/api/v1/enrich`: it reads a preloaded\nhot store only, so it never waits on the warehouse.\n\nThe identifier type is detected from its shape: 10 or 11 digits is a phone, 32 hex is an md5,\n40 hex is a sha1, 64 hex is a sha256.\n\n### Verify mode\n\nIf you already know who the lead claims to be, send `verify` with their name. We then return\nthe person **only if they agree**, and set `verified: true`. A mismatch comes back\n`matched: false` rather than handing you a stranger who happens to hold that number now.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"identifier":{"type":"string","description":"Phone (10 or 11 digits), md5, sha1, or sha256.","example":"4155550142"},"phone":{"type":"string","description":"Accepted for backward compatibility; prefer `identifier`."},"sha256":{"type":"string","description":"Accepted for backward compatibility; prefer `identifier`."},"mode":{"type":"string","enum":["enrich","rtb","identity"],"default":"enrich","description":"`enrich` returns the available PII and demographics. `rtb` returns ONLY `{ pass: boolean }` against your pre-qual filters, with no PII at all, for bid-time screening. `identity` returns hashes and provenance (md5, sha256, ip, source_url, registration_date) with no demographics."},"verify":{"type":"object","description":"Confirm-or-decline against a name you already hold.","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}}},"filters":{"type":"object","description":"Pre-qual filters for `rtb` mode. Also accepted at the top level.","properties":{"zip":{"type":"string"},"state":{"type":"string"},"age_min":{"type":"integer"},"age_max":{"type":"integer"},"income_min":{"type":"integer"},"credit_min":{"type":"integer"},"homeowner":{"type":"boolean"}}}},"required":["identifier"]},"examples":{"phone":{"summary":"Phone lookup","value":{"identifier":"4155550142"}},"verify":{"summary":"Confirm a lead you already have","value":{"identifier":"4155550142","verify":{"first_name":"Jane","last_name":"Doe"}}},"rtb":{"summary":"Bid-time screen, no PII returned","value":{"identifier":"4155550142","mode":"rtb","filters":{"state":"TX","age_min":25,"homeowner":true}}}}}}},"responses":{"200":{"description":"Lookup completed. Check `matched`.","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"boolean"},"verified":{"type":"boolean","description":"Present and true only when you sent `verify` and the person agreed."},"pass":{"type":"boolean","description":"`rtb` mode only. The single answer; no PII accompanies it."},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string","description":"Survived mailing address (golden profile preferred over the raw source row)."},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"age":{"type":"integer"},"gender":{"type":"string"},"marital_status":{"type":"string"},"homeowner":{"type":"string","description":"e.g. \"Home Owner\", \"Renter\". Absent when unknown."},"household_income":{"type":"integer","description":"Household income midpoint in USD."},"mobile_phone":{"type":"string"},"landline_phone":{"type":"string"},"email":{"type":"string","description":"The person's primary (rank-1) email, which may differ from the address you submitted."},"ip_address":{"type":"string"},"source_url":{"type":"string","description":"Where our graph last observed this person."},"md5":{"type":"string","description":"`identity` mode."},"sha256":{"type":"string","description":"`identity` mode."},"registration_date":{"type":"string","description":"`identity` mode."}},"required":["matched"]}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Realtime"],"operationId":"resolveDescriptor","summary":"Service descriptor (liveness probe)","security":[],"responses":{"200":{"description":"Service name, version, supported modes and identifier types."},"500":{"description":"Service unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/enrich":{"post":{"tags":["Enrichment"],"operationId":"enrich","summary":"Batch enrich by phone, email, IP, or record","description":"Submit up to 100 phones, up to 100 emails, and up to 25 IPs in a single call. The seeds are\nresolved in parallel and each returns its own array, so a request that sends only phones gets\nexactly the response it always got.\n\n### Emails\n\nSend a plaintext address **or** a pre-hashed md5, sha1, or sha256. All four resolve through the\nsame index, and `matched_by` tells you which form was used. This returns the full profile,\nunlike `/api/v1/append-phone`, which returns phone, name, and address only.\n\n### Phone confidence\n\nEvery matched phone row reports `confidence` (high, medium, or low), `verified`, `matched_by: \"phone\"`, and\n`phone_people`, the number of people our graph has on that number. Our identity graph comes first:\nwhen it holds a verified pairing of the number and a person, that person is returned ahead of any\nother association we hold, `verified` is true, and the confidence is high. Send `min_confidence: \"high\"`\nor `\"medium\"` when you only want results you can act on, for example scoring a live inbound call\nfrom the caller number alone. A match below the bar comes back as `matched: false` with\n`reason: \"below_min_confidence\"` and the band it had, carries no PII, and is not billed.\n\n### IPs\n\nAn IP tells you which household a hit came from, not reliably which person. Every IP row\nreports `ip_identities` and a `confidence` band, and shared carrier, proxy, or datacenter\naddresses are declined with `reason: \"shared_ip\"` rather than resolved to an arbitrary person\nbehind them.\n\n### Records\n\nSend `records` instead to run the name-gated waterfall over mixed identifiers you already hold\n(phone, email, name+address). Every match is confirmed against the name you supplied.\n\n### Field groups\n\nAdd `fields` to nest the full dictionary for the tabs you want. Groups are free; you are billed\nper matched record either way.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Provide at least one of `phones`, `emails`, `ips`, or `records`.","properties":{"phones":{"type":"array","maxItems":100,"items":{"type":"string"},"description":"Up to 100 phone numbers, any format."},"phone":{"type":"string","description":"Convenience form of a one-element `phones`."},"min_confidence":{"type":"string","enum":["high","medium"],"description":"Phones only. Return a phone match only when its `confidence` is at or above this band. A match below it comes back as `matched: false` with `reason: \"below_min_confidence\"` and is not billed. Any other value is a 400."},"emails":{"type":"array","maxItems":100,"items":{"type":"string"},"description":"Up to 100 plaintext addresses and/or md5 / sha1 / sha256 hashes, mixed freely."},"email":{"type":"string","description":"Convenience form of a one-element `emails`."},"ips":{"type":"array","maxItems":25,"items":{"type":"string"},"description":"Up to 25 IPv4 addresses."},"ip":{"type":"string","description":"Convenience form of a one-element `ips`."},"records":{"type":"array","maxItems":100,"description":"Up to 100 partial leads run through the name-gated waterfall.","items":{"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"}}}},"fields":{"type":"array","description":"Optional nested field groups to include on every matched row. Requesting groups does not change billing: a matched record counts once regardless of how many groups you ask for.","items":{"type":"string","enum":["demographics","property","financial","auto","auto_insurance","executive","geo","contacts","purchase_behavior","signals","behavior"]},"example":["demographics","property","auto"]}}},"examples":{"phones":{"summary":"Phones","value":{"phones":["4155550142","2125550188"]}},"live_call":{"summary":"Inbound caller, highest-confidence only","value":{"phones":["4155550142"],"min_confidence":"high"}},"emails":{"summary":"Plaintext and hashed emails together","value":{"emails":["jane@example.com","d41d8cd98f00b204e9800998ecf8427e"]}},"ips":{"summary":"Anonymous web hits","value":{"ips":["73.162.44.19"]}},"groups":{"summary":"With field groups","value":{"emails":["jane@example.com"],"fields":["demographics","property","auto"]}},"mixed":{"summary":"All three seeds in one call","value":{"phones":["4155550142"],"emails":["jane@example.com"],"ips":["73.162.44.19"]}}}}}},"responses":{"200":{"description":"Request accepted and processed. `matched` counts matches across every seed.","content":{"application/json":{"schema":{"type":"object","properties":{"requested":{"type":"integer","description":"Total identifiers submitted across all seeds."},"matched":{"type":"integer","description":"Total matched, and what you are billed for."},"results":{"type":"array","description":"Always present. Carries PhoneResult rows for a `phones` request, and RecordResult rows for a `records` request.","items":{"oneOf":[{"$ref":"#/components/schemas/PhoneResult"},{"$ref":"#/components/schemas/RecordResult"}]}},"email_results":{"type":"array","items":{"$ref":"#/components/schemas/EmailResult"},"description":"Present only when you submitted emails."},"ip_results":{"type":"array","items":{"$ref":"#/components/schemas/IpResult"},"description":"Present only when you submitted IPs."}},"required":["requested","matched"]}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Prepaid balance exhausted (wallet-billed accounts only). Add funds to resume.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/append-phone":{"post":{"tags":["Enrichment"],"operationId":"appendPhone","summary":"Append phone numbers from email or name and address","description":"The reverse of enrich: you hold an email or a postal identity and want the phone. Returns the\nmatched person's best mobile and landline plus name and address, with DNC.\n\nEmails must be **plaintext** here (we hash them ourselves). If you hold only a hash, use\n`/api/v1/enrich` with `emails`, which accepts md5, sha1, and sha256.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Provide `emails`, `records`, or both.","properties":{"emails":{"type":"array","maxItems":100,"items":{"type":"string"}},"email":{"type":"string"},"records":{"type":"array","maxItems":100,"items":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"email":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Matched people with their best phones.","content":{"application/json":{"schema":{"type":"object","properties":{"requested":{"type":"integer"},"matched":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"input":{"type":"string"},"matched":{"type":"boolean"},"confidence":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"phones":{"type":"array","items":{"type":"object","properties":{"phone":{"type":"string"},"type":{"type":"string"},"rank":{"type":"integer"},"carrier":{"type":"string"},"last_seen":{"type":"string"},"dnc":{"type":["integer","null"]},"best":{"type":"boolean"}}}}}}},"record_results":{"type":"array","description":"One per `records` item, in order, same shape as `results` plus `demographics` when the person matched: { age, gender, income_band, credit_range, homeowner }, each null where our store has no value. A name plus address is all it takes; no id is ever required.","items":{"type":"object"}}}}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Prepaid balance exhausted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/pull":{"post":{"tags":["Lists"],"operationId":"createPull","summary":"Queue a filtered list pull","description":"Submit ZIPs **or topic ids**, filters, and a hard record cap; we build the list, email\nthe CSV to the account, and store it for download. Returns immediately with 202 while\nthe pull runs.\n\n### Topic mode\n\nPass `topics` with ids from `GET /api/v1/topics/search` to order an in-market audience\ndirectly. ZIPs are optional in this mode, since intent audiences are usually national\nand narrowed by demographics instead.\n\nOnly the two intent universes (`intent_b2c`, `intent_b2b`) can be pulled on demand. The\ninbound, keyword, and responder universes are delivered as managed feeds; asking for one\nhere returns a 400 naming the specific topics involved.\n\n`recency_days` is clamped to the window we actually hold, and the response says so when\nit clamps. Asking for 30 days when 7 are loaded returns the real 7-day audience with a\nwarning, never 7 days relabeled as 30.\n\n### Filters go inside `filters`\n\nEvery filter belongs inside the `filters` object. `phone_requirement` in particular is\n`filters.phone_requirement`, NOT a top-level key. Anything we do not read is echoed back\nin `ignored_filters` with a warning, and that now includes keys sent at the top level of\nthe request: a top-level `phone_requirement` is not applied, it is reported, so you can\nresend it nested instead of paying for a broader audience than you asked for.\n\n### Refused, not shortened\n\nIf a prepaid balance or a daily cap cannot cover the full `max_records`, the request is\nrefused with 402 and the body carries `records_deliverable`. Nothing is pulled and\nnothing is charged. Either lower `max_records` to that number, or resend with\n`allow_partial: true` to accept the shorter file deliberately. A `max_records` exactly\nequal to what can be delivered is a complete order and runs normally.\n\n### Which number governs the account\n\nThe 202 carries a `billing` block naming the meter that actually applies.\n\n- `mode: \"monthly_plan\"` reports `monthly_limit`, `records_used` and `records_remaining`,\n  debited when the file is downloaded.\n- `mode: \"prepaid_wallet\"` reports `balance_usd`, `rate_per_record_usd` and\n  `records_deliverable` instead. A prepaid pull is charged against the balance and does\n  NOT increment plan usage, so `records_remaining` does not move for these accounts and\n  is not reported here. Watch `records_deliverable`.\n\nOn a monthly plan the request is also refused up front if it would exceed the remaining\nmonthly records, so there is no surprise overage on the API.\n\n### Stop paying for records you already bought\n\nSend `exclude_previously_delivered` as `phone`, `address` or `both` and we hold back records\nthis account has already been delivered. It is applied before the record cap, so a capped\npull still fills up with records that are new to you, and `GET /api/v1/pull` reports\n`previously_delivered_withheld` on the order. Records held back are never billed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"zips":{"type":"array","maxItems":1000,"items":{"type":"string"},"description":"Up to 1,000 ZIP codes. Required unless `topics` is given."},"topics":{"type":"array","maxItems":25,"items":{"type":"string"},"description":"Up to 25 topic ids from GET /api/v1/topics/search, ORed together. Intent universes only.","example":["b2c_7031","b2c_2207"]},"max_records":{"type":"integer","minimum":1,"maximum":100000,"description":"Required hard cap, 1 to 100,000."},"filters":{"type":"object","properties":{"min_home_value":{"type":"integer"},"max_home_value":{"type":"integer"},"year_built_on_or_after":{"type":"integer"},"year_built_on_or_before":{"type":"integer"},"min_owner_age":{"type":"integer"},"max_owner_age":{"type":"integer"},"require_email":{"type":"boolean"},"recency_days":{"type":"integer","description":"Topic mode. How far back to look for in-market activity. Clamped to the loaded window; the response discloses any clamp."},"states":{"type":"array","items":{"type":"string"},"description":"Topic mode. Two-letter state codes."},"exclude_states":{"type":"array","items":{"type":"string"},"description":"Topic mode. Two-letter state codes to exclude."},"min_hhi":{"type":"integer","description":"Topic mode. Minimum household income."},"max_hhi":{"type":"integer","description":"Topic mode. Maximum household income. Modeled band, skews high."},"homeowner":{"type":"boolean","description":"Topic mode. Confirmed homeowners only."},"credit_codes":{"type":"array","items":{"type":"string"},"description":"Topic mode. Credit range letters A (800+) through H (499-)."},"phone_requirement":{"type":"string","enum":["mobile","any","none"],"description":"BELONGS INSIDE `filters`, not at the top level of the request. Defaults to \"mobile\" when the output includes phone columns, so lists come back dialable. Pass \"none\" for the full list including rows with no phone. Sent at the top level it is NOT applied and is echoed back in `ignored_filters`."}}},"columns":{"type":"array","items":{"type":"string"},"description":"Optional column subset."},"allow_partial":{"type":"boolean","default":false,"description":"Opt in to a SHORT file. Default false: when a prepaid balance or daily cap cannot cover the full max_records the order is refused with 402 rather than truncated. Set true to accept records_deliverable records instead of being refused. Only the boolean true opts in."},"exclude_previously_delivered":{"type":"string","enum":["off","phone","address","both"],"description":"Hold back records this account has already been delivered, so you stop paying for records you already own. `phone` matches the number we handed over, `address` matches the household (which also catches the same house reached on a different line), `both` matches either. Omit it and your account setting applies, which is off unless you turned it on. On a ZIP pull it is applied BEFORE the record cap, so the file still fills up to max_records with records that are new to you; `GET /api/v1/pull` then reports `previously_delivered_withheld` for the order. Records held back are never billed."}},"required":["zips","max_records"]}}}},"responses":{"202":{"description":"Queued. The CSV is emailed to the account and listed by `GET /api/v1/pull`. Carries `order_id` and `cancel_url` so the order can be cancelled immediately, a `billing` block naming the meter this account is on, `ignored_filters` for anything not applied (nested or top-level), and, in topic mode, the resolved topics, the applied `recency_days` and any clamp warning."},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Refused, nothing pulled and nothing charged. Either `max_records` exceeds the records remaining on the monthly plan (body carries `records_remaining` and `monthly_limit`), or a prepaid balance / daily cap cannot cover it (body carries `records_deliverable`, `wallet`, and `how_to_proceed`). Resend with `allow_partial: true` to accept the shorter file instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The pull API is not enabled on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The pull would exceed the remaining monthly records on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Lists"],"operationId":"listPulls","summary":"List recent pull orders","description":"Newest first, PAGED: 20 orders per page by default. Pass `limit` (1 to 100) and `offset` to reach older orders; the response carries `total` and `has_more`. An order that was cancelled reports status \"canceled\", carries `canceled_at`, and has a null `download_url` because it was never billed and never delivered.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Orders per page, 1 to 100. Values above 100 are clamped to 100."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"How many orders to skip. Use with `limit` to page through the full history."}],"responses":{"200":{"description":"A page of orders with key-authenticated download links.","content":{"application/json":{"schema":{"type":"object","properties":{"orders":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"status":{"type":"string","description":"\"canceled\" whenever `canceled_at` is set."},"record_count":{"type":"integer"},"canceled_at":{"type":"string","format":"date-time","nullable":true,"description":"Present only on a cancelled order."},"download_url":{"type":"string","format":"uri","nullable":true,"description":"Null until the file exists, and always null on a cancelled order."}}}},"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer","description":"Total orders on the account."},"has_more":{"type":"boolean","description":"True when another page exists past this one."}}}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/pull/{id}/cancel":{"post":{"tags":["Lists"],"operationId":"cancelPull","summary":"Cancel a pull order","description":"Stop paying for an order you did not mean to place. `DELETE` on the same path behaves\nidentically.\n\n**What cancel does and does not promise.** Work starts the moment the order is placed, in\na process no later request can reach, so cancelling does NOT promise to interrupt a query\nalready running and this endpoint never claims it did. What it guarantees is the part that\ncosts money, and the guarantee is binding:\n\n- **Never charged**, on every kind of pull. The billing path checks the cancellation before\n  it debits, so there is no charge against the prepaid balance or the monthly plan.\n- **`download_url` stays null** for any cancelled order.\n- A cancelled **ZIP pull** also stops before it builds or sends anything: no email, no file.\n  On the topic and property engines a file already built may still be emailed. It is still\n  not billed.\n\nIdempotent: calling it again returns the same 200 body with `already_canceled: true`.\nScoped to the calling key's own account, so another account's order id is indistinguishable\nfrom one that does not exist.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The order id from the 202 or from GET /api/v1/pull."}],"responses":{"200":{"description":"Cancelled, or already cancelled. Not billed and not delivered either way.","content":{"application/json":{"schema":{"type":"object","properties":{"canceled":{"type":"boolean"},"already_canceled":{"type":"boolean","description":"True when a previous call had already cancelled it."},"order_id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"canceled"},"canceled_at":{"type":"string","format":"date-time"},"billed":{"type":"boolean","example":false},"delivered":{"type":"boolean","example":false},"note":{"type":"string"}}},"example":{"canceled":true,"already_canceled":false,"order_id":"4b9eee20-5d9c-45d7-a8a3-308be83c7905","status":"canceled","billed":false,"delivered":false}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such order on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The order already completed (delivered and billed) or already failed, so there is nothing to cancel.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Lists"],"operationId":"cancelPullDelete","summary":"Cancel a pull order (alias of POST)","description":"Identical to `POST /api/v1/pull/{id}/cancel`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled, or already cancelled. Not billed and not delivered."},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such order on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The order already completed or already failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/relay/v1/audiences":{"post":{"tags":["Lists"],"operationId":"createAudience","summary":"Submit a ZIP audience job","description":"Asynchronous: returns 202 with a `job_id`, then poll `GET /api/relay/v1/audiences/{job_id}` until it completes and carries a CSV link. Supply `idempotency_key` to make retries safe.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"zips":{"type":"array","maxItems":50,"items":{"type":"string"}},"min_year_built":{"type":"integer"},"max_year_built":{"type":"integer"},"min_home_value":{"type":"integer"},"max_home_value":{"type":"integer"},"min_sqft":{"type":"integer"},"min_owner_age":{"type":"integer"},"max_owner_age":{"type":"integer"},"require_email":{"type":"boolean"},"phone_requirement":{"type":"string","enum":["any","cell_or_landline","cell"]},"max_records":{"type":"integer","minimum":1,"maximum":1000000},"columns":{"type":"array","items":{"type":"string"}},"callback_url":{"type":"string","format":"uri","description":"Optional webhook fired when the job completes."},"idempotency_key":{"type":"string","description":"Re-submitting the same key returns the original job instead of a duplicate."}},"required":["zips"]}}}},"responses":{"202":{"description":"Accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string"},"estimated_records":{"type":"integer"},"estimated_seconds":{"type":"integer"},"poll_url":{"type":"string"}}}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Lists"],"operationId":"listAudiences","summary":"List this partner's audience jobs","responses":{"200":{"description":"Recent jobs."},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/relay/v1/audiences/{job_id}":{"get":{"tags":["Lists"],"operationId":"getAudience","summary":"Poll one audience job","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job status. When complete it carries `row_count` and `download_url`.","content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["queued","running","completed","failed"]},"row_count":{"type":"integer"},"download_url":{"type":"string","format":"uri"}}}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such job for this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/topics/search":{"get":{"tags":["Discovery"],"operationId":"searchTopics","summary":"Search everything you can target","description":"Roughly 50,000 targetable topics across seven universes: consumer and business\nin-market intent, keyword intent by funnel stage, inbound lead and caller verticals,\nand responder pools. Search returns the topic ids you then pass to a pull or an\naudience build.\n\nCall it with no parameters to get the universe summary, which is the fastest way to\nsee what is on the shelf.\n\n### Reach\n\n`reach` is how many people are addressable for that topic. It is **omitted** for live\ndaily flows, where no total is meaningful; those return records continuously rather\nthan from a fixed pool.\n\n### Billing\n\nThis endpoint is metadata only: topic names and counts, no PII and no records. It does\nnot bill and does not count against your record cap. Browse freely, pay when you pull.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Free text, matched against label, description, category, and subcategory. An exact topic id also works.","example":"solar"},{"name":"universe","in":"query","schema":{"type":"string"},"description":"Comma-separated filter. One or more of: intent_b2c, intent_b2b, keyword, inbound_leads, inbound_calls, inbound_live, responders.","example":"inbound_leads,responders"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Restrict to a category, e.g. \"Health & Wellness\"."},{"name":"min_reach","in":"query","schema":{"type":"integer"},"description":"Only topics reaching at least this many people."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Matching topics, best match first, then by reach.","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":["string","null"]},"total":{"type":"integer","description":"Total matches, not just this page."},"limit":{"type":"integer"},"offset":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/Topic"}},"universes":{"type":"array","description":"Returned instead of `results` when you call with no parameters.","items":{"type":"object","properties":{"universe":{"type":"string"},"label":{"type":"string"},"topics":{"type":"integer"},"reach":{"type":"integer"}}}}}}}}},"400":{"description":"Unknown universe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Search unavailable. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/usage":{"get":{"tags":["Account"],"operationId":"getUsage","summary":"Month-to-date usage and remaining limit","description":"Usage rolls up across every key on the account and is compared against the account limit. A null limit means no fixed cap (usage-based or enterprise terms). Prepaid accounts also see live wallet balance.","responses":{"200":{"description":"Usage report.","content":{"application/json":{"schema":{"type":"object","properties":{"month_to_date":{"type":"integer"},"last_7_days":{"type":"integer"},"monthly_limit":{"type":["integer","null"]},"remaining":{"type":["integer","null"]},"wallet":{"type":"object","description":"Prepaid accounts only.","properties":{"balance_usd":{"type":"number"},"rate_per_record_usd":{"type":"number"}}}}}}}},"401":{"description":"Missing or malformed API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Key is not yet linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/website-visitors":{"get":{"tags":["Pixel"],"operationId":"websiteVisitorsCount","summary":"Count and shape the people your pixel resolved","description":"Your website visitor database, as numbers: how many distinct people, how many carry an email,\nan address, a phone, a cell phone, and which states and pages they came from. Add filters as\nquery parameters to size a selection before you export it. Free, and nothing is charged.\n\nThe same export is one click in the app: Pixel menu, **View your website funnel**, then\n**Export CSV**.\n\nBefore the pixel has resolved anyone the response is `{ \"available\": false, \"message\": \"...\" }`\nwith a 200, not an error.","parameters":[{"name":"states","in":"query","required":false,"schema":{"type":"string"},"description":"Two-letter state codes. Comma-separated on GET."},{"name":"cities","in":"query","required":false,"schema":{"type":"string"},"description":"City names. Comma-separated on GET."},{"name":"zips","in":"query","required":false,"schema":{"type":"string"},"description":"Five-digit ZIPs. Comma-separated on GET."},{"name":"min_age","in":"query","required":false,"schema":{"type":"integer"}},{"name":"max_age","in":"query","required":false,"schema":{"type":"integer"}},{"name":"min_hhi","in":"query","required":false,"schema":{"type":"integer"},"description":"Minimum household income, in dollars."},{"name":"max_hhi","in":"query","required":false,"schema":{"type":"integer"},"description":"Maximum household income, in dollars."},{"name":"page_contains","in":"query","required":false,"schema":{"type":"string"},"description":"Only people who viewed a page whose path contains this text."},{"name":"domain","in":"query","required":false,"schema":{"type":"string"},"description":"Only people seen on this website, when the pixel runs on more than one."},{"name":"seen_within_days","in":"query","required":false,"schema":{"type":"integer"},"description":"Only people seen in the last N days."},{"name":"converted_only","in":"query","required":false,"schema":{"type":"boolean"},"description":"Only people who did more than browse (a form submit or other tracked conversion)."},{"name":"require_email","in":"query","required":false,"schema":{"type":"boolean"},"description":"Only people with an email on file."},{"name":"require_phone","in":"query","required":false,"schema":{"type":"boolean"},"description":"Only people with a phone on file."},{"name":"exclude_previously_delivered","in":"query","required":false,"schema":{"type":"boolean"},"description":"Default false. When true, people this account has already been sent in an earlier export are held back, so a repeating pull is genuinely incremental. Somebody who keeps visiting matches a rolling `seen_within_days` window every day, and export is charged per delivered record, so without this you receive and pay for that person again on every run. Counting applies it the same way, so a GET with the flag predicts exactly what a POST with the flag will deliver, and both report `already_delivered`."}],"responses":{"200":{"description":"Counts for the selection. `people` is distinct people, never visits.","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"message":{"type":"string","description":"Only when `available` is false."},"selection":{"type":"string","description":"The filters you sent, in one sentence."},"people":{"type":"integer"},"already_delivered":{"type":"integer","description":"Only when you sent `exclude_previously_delivered`. People who matched everything else and were held back because this account has already been sent them. Not counted in `people`."},"already_delivered_note":{"type":"string","description":"Only when you sent `exclude_previously_delivered`."},"with_email":{"type":"integer"},"with_address":{"type":"integer"},"did_more_than_browse":{"type":"integer","description":"People with a tracked conversion (a form submit, for example)."},"with_phone":{"type":"integer"},"with_cell_phone":{"type":"integer"},"phone_no_dnc_flag":{"type":"integer","description":"Numbers we hold no do-not-call flag for. Not the same as verified callable."},"phone_household_only":{"type":"integer","description":"Numbers filed against the household rather than the person."},"top_states":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string"},"people":{"type":"integer"}}}},"top_pages":{"type":"array","items":{"type":"object","properties":{"page":{"type":"string"},"people":{"type":"integer"}}}},"most_recent_visit":{"type":"string","format":"date"},"total_people_in_database":{"type":"integer","description":"Everyone, before filters."},"notes":{"type":"string"},"phone_notes":{"type":"string"}},"required":["available"]},"example":{"available":true,"selection":"everyone in your website-visitor database","people":73,"with_email":73,"with_address":73,"did_more_than_browse":34,"with_phone":23,"with_cell_phone":17,"phone_no_dnc_flag":17,"phone_household_only":0,"top_states":[{"state":"FL","people":9},{"state":"CA","people":8}],"top_pages":[{"page":"/","people":48},{"page":"/signup","people":19}],"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. phone_household_only counts numbers filed against the person household rather than against the person, so they may belong to someone else at the same address. Every delivered row carries phone_type, phone_dnc and phone_match so you can see which is which."}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Key is not yet linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Pixel"],"operationId":"websiteVisitorsExport","summary":"Export your website visitors as a CSV, with email and phone","description":"Send the same filters as JSON plus a required `max_records`, and we build a CSV of the matching\npeople and return a secure download link. The rows are never in the response body: a list\nbelongs in a file you download, not in something that ends up in a log.\n\nColumns, in order: `first_name`, `last_name`, `email`, `address`, `city`, `state`, `zip`, `age`, `household_income`, `first_seen`, `last_seen`, `visits`, `pages_viewed`, `last_page`, `converted`, `website`, `phone`, `phone_type`, `phone_dnc`, `phone_match`.\n\nA phone never ships as digits alone. `phone_type` says whether it is a cell, `phone_dnc` whether\nit carries a do-not-call flag, and `phone_match` whether it is that person's own line or their\nhousehold's, so you can decide what is dialable.\n\nCustomers can export up to 200,000 people per call. The link is valid for 24 hours.\n\n**Pulling on a schedule?** Send `exclude_previously_delivered: true`. Export is charged per\ndelivered record, and somebody who keeps visiting matches a rolling `seen_within_days` window\nevery single day, so without it a daily pull delivers and charges for that person every day.\nWith it on, each person leaves once and never appears in a later window, and the response\nreports `already_delivered` so you can see how many were held back. GET applies it identically,\nso you can size the file for free before you take it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"states":{"type":"array","items":{"type":"string"},"description":"Two-letter state codes. Comma-separated on GET."},"cities":{"type":"array","items":{"type":"string"},"description":"City names. Comma-separated on GET."},"zips":{"type":"array","items":{"type":"string"},"description":"Five-digit ZIPs. Comma-separated on GET."},"min_age":{"type":"integer"},"max_age":{"type":"integer"},"min_hhi":{"type":"integer","description":"Minimum household income, in dollars."},"max_hhi":{"type":"integer","description":"Maximum household income, in dollars."},"page_contains":{"type":"string","description":"Only people who viewed a page whose path contains this text."},"domain":{"type":"string","description":"Only people seen on this website, when the pixel runs on more than one."},"seen_within_days":{"type":"integer","description":"Only people seen in the last N days."},"converted_only":{"type":"boolean","description":"Only people who did more than browse (a form submit or other tracked conversion)."},"require_email":{"type":"boolean","description":"Only people with an email on file."},"require_phone":{"type":"boolean","description":"Only people with a phone on file."},"exclude_previously_delivered":{"type":"boolean","description":"Default false. When true, people this account has already been sent in an earlier export are held back, so a repeating pull is genuinely incremental. Somebody who keeps visiting matches a rolling `seen_within_days` window every day, and export is charged per delivered record, so without this you receive and pay for that person again on every run. Counting applies it the same way, so a GET with the flag predicts exactly what a POST with the flag will deliver, and both report `already_delivered`."},"max_records":{"type":"integer","minimum":1,"maximum":200000,"description":"Required. The most rows to deliver."}},"required":["max_records"]},"examples":{"converters":{"summary":"Everyone who converted in the last 30 days, with a phone","value":{"seen_within_days":30,"converted_only":true,"require_phone":true,"max_records":5000}},"texas":{"summary":"Texas visitors 35 and up","value":{"states":["TX"],"min_age":35,"max_records":20000}},"daily":{"summary":"A once-a-day incremental pull: only people you have not been sent before","value":{"seen_within_days":30,"exclude_previously_delivered":true,"max_records":50000}}}}}},"responses":{"200":{"description":"The file is ready. When nobody matches, `delivered` is false, `rows` is 0, and nothing was charged.","content":{"application/json":{"schema":{"type":"object","properties":{"delivered":{"type":"boolean"},"rows":{"type":"integer","description":"Rows in the file. One per person."},"matched_before_cap":{"type":"integer","description":"How many matched before `max_records` was applied."},"capped":{"type":"boolean","description":"True when `max_records` cut the selection."},"selection":{"type":"string"},"already_delivered":{"type":"integer","description":"Only when you sent `exclude_previously_delivered`. People who matched everything else and were held back because this account has already been sent them. This is why today's file can be smaller than yesterday's."},"filename":{"type":"string"},"download_url":{"type":"string","format":"uri","description":"Secure link, valid 24 hours."},"order_id":{"type":"string","format":"uuid"},"list_id":{"type":["string","null"],"description":"The handle for this delivery. It is also the last column of every row in the file (`List_ID`), and it is on every file we send whoever you are. If you are on a revenue share with us, quote it to POST /api/v1/revenue when you report what the file earned, which is how revenue is tied back to the exact people it came from. On a paid plan there is nothing to report and the id is simply how you and we name this delivery."},"note":{"type":"string"},"message":{"type":"string","description":"Only when `delivered` is false."}},"required":["delivered"]},"example":{"delivered":true,"rows":34,"matched_before_cap":34,"capped":false,"selection":"people who did more than browse, 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","list_id":"WEBVISITOR-2F1C-20260908-2F1C0E4A","note":"Secure link, valid 24 hours. One row per person. Every row carries List_ID WEBVISITOR-2F1C-20260908-2F1C0E4A. On a revenue share the note also asks you to quote it to POST /api/v1/revenue when you report what this file made."}}}},"400":{"description":"Body is not JSON, or `max_records` is missing, not a positive integer, or above the ceiling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Prepaid balance empty (`error: \"wallet_empty\"`, with a `message`). Nothing was selected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Key is not yet linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/pull/{id}/download":{"get":{"tags":["Lists"],"operationId":"downloadPull","summary":"Download a completed pull as CSV","description":"The file is byte-for-byte what was emailed to the account. The order must belong to the key's account. Orders from `/api/v1/pull`, `/api/v1/property-pull`, and `/api/v1/aged-optin` all download here.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The order id from `GET /api/v1/pull`."}],"responses":{"200":{"description":"The CSV, as an attachment named `tdtm_pull_<first 8 of id>.csv`.","content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"The id is not a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such order on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The order has no file yet. The body names its status; poll `GET /api/v1/pull`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"File unavailable. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/property-pull":{"post":{"tags":["Lists"],"operationId":"propertyPull","summary":"Estimate or order a property-filtered homeowner list","description":"Homeowners in single-family homes, narrowed by income floor, home value window, year-built\nwindow, and equity (the share of the home they own). Send `estimate: true` for a free funnel\nof counts; send `max_records` instead to order the list. Your saved suppression lists are\nalready reflected in both.\n\n### DNC is a flag, never a filter\n\nNumbers on the do-not-call registry are delivered with a `DNC` column set to Y and counted on\nboth sides of the split (`dnc_flagged` and `callable_non_dnc`), so your compliance decides.\nNothing is silently scrubbed.\n\n### Stop paying for records you already bought\n\nSend `exclude_previously_delivered` as `phone`, `address` or `both` and we hold back records this\naccount has already been delivered: the phone number we handed over, the household (which also\ncatches the same house reached on a different line), or either. It is applied inside the same\nselection query for the estimate and for the order, so the count you are quoted is the count\nthat ships, and both report `previously_delivered_withheld`. Omit the field and your account\nsetting applies, which is off unless you turned it on. Records held back are never billed.\n\n### A filter we cannot apply is stated, not hidden\n\nWhen an estimate ignores a filter, the response carries `ignored_filters` and a `warning`, and\nthe count answers a broader question than you asked. Do not quote it as if the filter applied.\n\n### What the delivered file proves\n\nThe file ends with `Owner Status` (`Home Owner` or `Probable Home Owner`) and `Dwelling Type`\n(`Single Family` and similar), the two fields the selection is actually built on, so the\nhomeowner and single-family selection is verifiable row by row instead of taken on trust.\nBoth were appended after the existing columns, so no column an importer already reads moved.\n\n### Estimates\n\nAn estimate is free of charge: it never touches your prepaid balance or your monthly plan, and\nthere is no per-estimate fee. It is not unconditional, though. A key that is not linked to an\naccount is refused with 403, and if your saved suppression lists cannot be read the estimate\nreturns 503 rather than a count that quietly ignores them.\n\nOrdering needs list-pull access on the account; the order is refused up front if it would\nexceed the records remaining on your monthly plan.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"estimate":{"type":"boolean","description":"True for counts only. Omit (and send `max_records`) to order."},"max_records":{"type":"integer","minimum":1,"maximum":100000,"description":"Required to order: 1 to 100,000."},"states":{"type":"array","items":{"type":"string"}},"zips":{"type":"array","items":{"type":"string"},"description":"Five-digit ZIPs, or three-digit prefixes."},"cities":{"type":"array","items":{"type":"string"}},"min_hhi":{"type":"integer","description":"Household income floor, in dollars."},"min_home_value":{"type":"integer"},"max_home_value":{"type":"integer"},"year_built_min":{"type":"integer"},"year_built_max":{"type":"integer"},"min_equity_pct":{"type":"integer","description":"Owns at least this percent of the home."},"single_family_only":{"type":"boolean","default":true},"cell_scope":{"type":"string","enum":["primary","all_cells"],"default":"primary","description":"One primary cell per person, or every cell on file."},"geo_label":{"type":"string","description":"A name for the order, shown in your history."},"exclude_previously_delivered":{"type":"string","enum":["off","phone","address","both"],"description":"Hold back records this account has already been delivered, so you stop paying for records you already own. `phone` matches the number we handed over, `address` matches the household (which also catches the same house reached on a different line), `both` matches either. Omit it and your account setting applies, which is off unless you turned it on. The estimate and the order apply it in the SAME query, and both report `previously_delivered_withheld`."}}},"examples":{"estimate":{"summary":"Free estimate","value":{"estimate":true,"states":["MI"],"min_home_value":200000}},"order":{"summary":"Order 10,000","value":{"states":["MI"],"min_home_value":200000,"min_equity_pct":30,"max_records":10000}}}}}},"responses":{"200":{"description":"Estimate. Each count is a step down the funnel.","content":{"application/json":{"schema":{"type":"object","properties":{"selection":{"type":"string"},"people_in_geo":{"type":"integer"},"homeowner_base":{"type":"integer"},"with_qualifying_property":{"type":"integer"},"value_qualified":{"type":"integer"},"equity_qualified":{"type":"integer"},"unique_cells":{"type":"integer","description":"Deliverable unique mobile numbers, DNC-flagged numbers included."},"dnc_flagged":{"type":"integer"},"callable_non_dnc":{"type":"integer"},"previously_delivered_withheld":{"type":"integer","description":"Unique numbers held back because this account has already been delivered them. Already subtracted from `unique_cells`, so what you see is what an order ships. 0 when the option is off."},"exclude_previously_delivered":{"type":"string","description":"The rule that was applied to this count: off, phone, address or both."},"notes":{"type":"string"},"ignored_filters":{"type":"array","items":{"type":"string"}},"warning":{"type":"string"}}},"example":{"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."}}}},"202":{"description":"Order queued. The CSV is emailed and listed by `GET /api/v1/pull` with a download link when ready. Every row of every file ends with a `List_ID` column, the delivery id to quote when reporting revenue on that file.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"order_id":{"type":"string","format":"uuid"},"max_records":{"type":"integer"},"delivery_email":{"type":"string"},"dnc_behavior":{"type":"string"},"exclude_previously_delivered":{"type":"string","description":"The rule this order was accepted with."},"dedupe_behavior":{"type":"string","description":"Present when records already delivered to you are being held back."},"note":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Bad JSON, an invalid filter, or `max_records` missing when ordering (\"max_records\" is required: 1 to 100,000).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`max_records` exceeds the records remaining this month, or the prepaid balance cannot cover it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account, or list pulls over the API are not enabled on it. Estimates still work.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The count timed out or failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/aged-form-fills":{"post":{"tags":["Lists"],"operationId":"agedFormFills","summary":"Estimate or order aged opt-in form fills for any vertical by form date","description":"People who filled out a form for a vertical (mortgage, mortgage_refi, solar, home_services,\nhome_warranty, home_insurance, home_sellers, auto_insurance, auto_warranty, tax_debt, roofing,\nwindows, bathroom_remodel), filtered by when they filled it: a date window, or a lead age such\nas 6 to 12 months. Self-serve for every account.\n\n`mode: \"estimate\"` (the default) is free and changes nothing. `mode: \"order\"` queues the list\nthrough the same runner as every other pull: the CSV is emailed, listed by `GET /api/v1/pull`,\nand downloadable from `/api/v1/pull/{id}/download`.\n\n### Counts are people\n\nEvery count and every row is one person (phone, else email, else name and ZIP), newest form fill\nwins, so the count you are quoted is the count that ships. `rows` is the raw row count beside it.\n`with_form_date` says how many carry a date; a window drops undated rows unless `include_undated`\nis true. The phone is as supplied and not DNC-scrubbed. No date of birth is ever delivered.\n\nFor a second batch of the same selection, order again with `offset` set to the number already delivered.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vertical"],"properties":{"mode":{"type":"string","enum":["estimate","order"],"default":"estimate"},"vertical":{"type":"string","description":"One vertical; synonyms such as \"refi\" or \"hvac\" are mapped."},"states":{"type":"array","items":{"type":"string"}},"cities":{"type":"array","items":{"type":"string"}},"zips":{"type":"array","items":{"type":"string"},"description":"Five-digit ZIPs or three-digit prefixes."},"opt_in_from":{"type":"string","description":"Earliest form date, YYYY-MM-DD."},"opt_in_to":{"type":"string","description":"Latest form date, YYYY-MM-DD."},"min_age_months":{"type":"integer","description":"Only leads at least this many months old."},"max_age_months":{"type":"integer","description":"Only leads at most this many months old."},"include_undated":{"type":"boolean","description":"Keep rows with no form date when a window is set."},"require_phone":{"type":"boolean"},"require_email":{"type":"boolean"},"require_consent":{"type":"boolean","description":"TrustedForm cert or Jornaya id present."},"max_records":{"type":"integer","description":"Required for mode \"order\": 1 to 200,000."},"offset":{"type":"integer"},"label":{"type":"string"}}}}}},"responses":{"200":{"description":"Estimate.","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate"]},"selection":{"type":"string"},"people":{"type":"integer"},"rows":{"type":"integer"},"with_phone":{"type":"integer"},"with_email":{"type":"integer"},"with_consent":{"type":"integer","description":"People holding a VALIDATED consent artifact: a retrievable https://cert.trustedform.com/<id> URL, or a real Jornaya id. A populated-but-unusable value (a bare hex hash, an all-zero GUID, a stray domain) does not count and never meant anything."},"with_trustedform":{"type":"integer","description":"The TrustedForm half on its own, always reported. On mortgage this is about 1.3% of the vertical, far below with_consent, which also counts Jornaya."},"with_form_date":{"type":"integer"},"with_form_time":{"type":"integer","description":"Of the dated people, how many carry a time of day and not just a date. Under 1% on mortgage: a form date here is a calendar day the supplier asserts, not a timestamped consent capture."},"newest_form_date":{"type":"string","nullable":true},"oldest_form_date":{"type":"string","nullable":true},"by_form_month":{"type":"array","items":{"type":"object","properties":{"month":{"type":"string"},"people":{"type":"integer"}}}},"by_state":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string"},"people":{"type":"integer"}}}},"notes":{"type":"string"}}}}}},"202":{"description":"Order queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"mode":{"type":"string","enum":["order"]},"selection":{"type":"string"},"max_records":{"type":"integer"},"estimated_records":{"type":"integer"},"offset":{"type":"integer"},"available_after_offset":{"type":"integer"},"columns":{"type":"array","items":{"type":"string"},"description":"First_Name, Last_Name, Address, City, State, Zip, Phone, Email, Lead_Date, Vertical, Consent_Cert, Jornaya_ID, Supplier_Form_URL, Appended_Opt_In_URL, Supplier_IP_Address, Appended_Opt_In_IP, Credit_Rating, Homeowner_Status, Loan_Amount, Loan_Purpose, Property_Value, then List_ID last: the delivery id on every row, which is what you report revenue against. Supplier_Form_URL and Supplier_IP_Address are the lead's own, exactly as the supplier sent them, and Supplier_Form_URL is empty on every row today because no supplier has delivered form URLs yet. Appended_Opt_In_URL and Appended_Opt_In_IP are OURS: opt-in evidence from our own email graph for the same person, describing a different opt-in event a median 7.9 years from Lead_Date and commonly a survey, sweepstakes or rewards page. They are not this lead's consent artifact and must not be presented as the page the lead filled out. Until 2026-09-16 these shipped as Opt_In_URL and IP_Address; an importer keyed on those two header names needs updating."},"delivery_email":{"type":"string"},"note":{"type":"string"},"wallet":{"type":"object"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"`vertical` is missing or unknown, a date is malformed, `max_records` is missing for an order, or nothing is left past `offset`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`max_records` exceeds the records remaining this month, or the prepaid balance cannot cover it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account, or the account is not eligible for API pulls.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/credit-triggers":{"post":{"tags":["Lists"],"operationId":"creditTriggers","summary":"Estimate or order records from the daily credit trigger file","description":"People whose credit file just moved (a new inquiry or trade), one file a day, with name, address,\nphone, FICO and the open mortgage trade count on every row. Licensed per account: the\n`credit_triggers` entitlement gates both modes.\n\n`mode: \"estimate\"` (the default) is free and changes nothing. `mode: \"order\"` queues the list\nthrough the same runner as every other pull: the CSV is emailed, listed by `GET /api/v1/pull`,\nand downloadable from `/api/v1/pull/{id}/download`.\n\nDo-not-call phones are dropped unless `include_dnc` is true; every delivered row carries `DNC` and\n`Phone_Type` (mobile or landline) from our phone store. No SSN, date of birth or account numbers are\never delivered; age is delivered instead of a birth date.\n\nFor a second batch of the same selection, order again with `offset` set to the number already delivered.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate","order"],"default":"estimate"},"trigger_days":{"type":"integer","description":"Only triggers from the last N days on file (1 to 180). Default 30."},"trigger_from":{"type":"string","description":"YYYY-MM-DD, overrides trigger_days."},"trigger_to":{"type":"string","description":"YYYY-MM-DD."},"fico_min":{"type":"integer"},"fico_max":{"type":"integer"},"states":{"type":"array","items":{"type":"string"}},"zips":{"type":"array","items":{"type":"string"},"description":"Five-digit ZIPs or three-digit prefixes."},"require_mortgage":{"type":"boolean","description":"At least one open mortgage trade."},"require_email":{"type":"boolean"},"require_mobile":{"type":"boolean","description":"Phone types as mobile in our phone store."},"include_dnc":{"type":"boolean","description":"Keep do-not-call phones (dropped by default)."},"age_min":{"type":"integer"},"age_max":{"type":"integer"},"max_records":{"type":"integer","description":"Required for mode \"order\": 1 to 200,000."},"offset":{"type":"integer"},"label":{"type":"string"}}}}}},"responses":{"200":{"description":"Estimate.","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate"]},"selection":{"type":"string"},"mailable_households":{"type":"integer"},"with_phone":{"type":"integer"},"with_email":{"type":"integer"},"with_vin":{"type":"integer"},"by_vehicle_year":{"type":"array","items":{"type":"object","properties":{"year":{"type":"integer"},"households":{"type":"integer"}}}},"notes":{"type":"string"}}}}}},"202":{"description":"Order queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"mode":{"type":"string","enum":["order"]},"selection":{"type":"string"},"max_records":{"type":"integer"},"estimated_records":{"type":"integer"},"offset":{"type":"integer"},"available_after_offset":{"type":"integer"},"columns":{"type":"array","items":{"type":"string"},"description":"First_Name, Last_Name, Address, City, State, Zip, Vehicle_Make, Vehicle_Model, Vehicle_Year, VIN, Lead_Date (the date the consumer filled this form, YYYY-MM-DD), Opt_In_Date (the older opt-in date the source holds for the person, separate so it is never read as the inquiry), Received_Date (the day we received the record; the form was filled on or before it), Lead_IP, Lead_Site and DOB, then Age, Gender, Income, Credit and Homeowner from our identity store on the rows that match by exact name, house number and zip (blank means no match) (what else the form carried; blank where the source did not send it), plus Phone and Email when `include_contact` is true, then List_ID last: the delivery id on every row, which is what you report revenue against."},"delivery_email":{"type":"string"},"note":{"type":"string"},"wallet":{"type":"object"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"`mode` is not estimate or order, a vehicle filter is invalid, `max_records` is missing for an order, or nothing is left past `offset`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`max_records` exceeds the records remaining this month, or the prepaid balance cannot cover it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account, or the account is not eligible for API pulls.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/aged-optin":{"post":{"tags":["Lists"],"operationId":"agedOptin","summary":"Estimate or order aged opt-in auto records by vehicle","description":"Aged opt-in auto insurance or auto warranty records, filtered by the vehicle on the form:\nmodel-year range, makes and models to include or exclude, heavy-duty exclusion. Built for\ndirect mail.\n\n`mode: \"estimate\"` (the default) is free and changes nothing. `mode: \"order\"` queues the list\nthrough the same runner as every other pull: the CSV is emailed, listed by `GET /api/v1/pull`,\nand downloadable from `/api/v1/pull/{id}/download`.\n\n### Counts are mailable households\n\nEvery count and every row is a mailable household: name, street address, and ZIP present, one\nrow per household, newest form fill wins. That is smaller than a raw record count on purpose,\nbecause the count you are quoted has to be the count that ships. VIN coverage is low on this\nsource; quote `with_vin` before promising VINs. The phone is as supplied and is not DNC-scrubbed:\nit is for record matching, not dialing.\n\nFor a second batch of the same selection, order again with `offset` set to the number already delivered.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate","order"],"default":"estimate"},"vertical":{"type":"string","enum":["auto_insurance","auto_warranty"],"default":"auto_insurance"},"states":{"type":"array","items":{"type":"string"}},"zips":{"type":"array","items":{"type":"string"},"description":"Five-digit ZIPs or three-digit prefixes."},"cities":{"type":"array","items":{"type":"string"}},"vehicle_year_min":{"type":"integer"},"vehicle_year_max":{"type":"integer"},"vehicle_makes":{"type":"array","items":{"type":"string"}},"exclude_vehicle_makes":{"type":"array","items":{"type":"string"}},"vehicle_models":{"type":"array","items":{"type":"string"}},"exclude_vehicle_models":{"type":"array","items":{"type":"string"}},"exclude_heavy_duty":{"type":"boolean"},"require_phone":{"type":"boolean"},"require_lead_date":{"type":"boolean","description":"Only records whose source sent the date the form was filled (Lead_Date). Check with_lead_date on the estimate first."},"require_email":{"type":"boolean"},"require_vin":{"type":"boolean"},"max_records":{"type":"integer","minimum":1,"maximum":200000,"description":"Required for `order`: 1 to 200,000."},"offset":{"type":"integer","description":"Order mode. Skip this many households, for a follow-on batch."},"include_contact":{"type":"boolean","description":"Order mode. Add Phone and Email columns to the mail file."},"label":{"type":"string","maxLength":80,"description":"Order mode. A name for the order."}}},"examples":{"estimate":{"summary":"Newer vehicles in Texas","value":{"mode":"estimate","states":["TX"],"vehicle_year_min":2018}},"order":{"summary":"Order 25,000 mail pieces","value":{"mode":"order","states":["TX"],"vehicle_year_min":2018,"exclude_heavy_duty":true,"max_records":25000}}}}}},"responses":{"200":{"description":"Estimate.","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate"]},"selection":{"type":"string"},"mailable_households":{"type":"integer"},"with_phone":{"type":"integer"},"with_email":{"type":"integer"},"with_vin":{"type":"integer"},"by_vehicle_year":{"type":"array","items":{"type":"object","properties":{"year":{"type":"integer"},"households":{"type":"integer"}}}},"notes":{"type":"string"}}}}}},"202":{"description":"Order queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"mode":{"type":"string","enum":["order"]},"selection":{"type":"string"},"max_records":{"type":"integer"},"estimated_records":{"type":"integer"},"offset":{"type":"integer"},"available_after_offset":{"type":"integer"},"columns":{"type":"array","items":{"type":"string"},"description":"First_Name, Last_Name, Address, City, State, Zip, Vehicle_Make, Vehicle_Model, Vehicle_Year, VIN, Lead_Date (the date the consumer filled this form, YYYY-MM-DD), Opt_In_Date (the older opt-in date the source holds for the person, separate so it is never read as the inquiry), Received_Date (the day we received the record; the form was filled on or before it), Lead_IP, Lead_Site and DOB, then Age, Gender, Income, Credit and Homeowner from our identity store on the rows that match by exact name, house number and zip (blank means no match) (what else the form carried; blank where the source did not send it), plus Phone and Email when `include_contact` is true, then List_ID last: the delivery id on every row, which is what you report revenue against."},"delivery_email":{"type":"string"},"note":{"type":"string"},"wallet":{"type":"object"},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"`mode` is not estimate or order, a vehicle filter is invalid, `max_records` is missing for an order, or nothing is left past `offset`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`max_records` exceeds the records remaining this month, or the prepaid balance cannot cover it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account, or the account is not eligible for API pulls.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/feeds":{"get":{"tags":["Feeds"],"operationId":"listFeeds","summary":"Your feeds, described in plain English","description":"Every feed on the account with the same description every other surface renders: what it\ndelivers, what the records are made of, every active filter, the cap, the schedule, how\nrepeats are handled, and where it goes (credentials removed). `recent_changes` is the last\nten changes, each saying who made it. Read-only and free.","responses":{"200":{"description":"Feeds on the account.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"feeds":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Pass to `/api/v1/feeds/change` and `/api/v1/feeds/control`."},"name":{"type":"string"},"active":{"type":"boolean"},"age_floor":{"type":["integer","null"]},"description":{"type":"string","description":"Every characteristic below, joined into one paragraph."},"characteristics":{"type":"object","description":"The same plain-English description every other surface renders for this feed.","properties":{"delivers":{"type":"string"},"composition":{"type":"object","description":"What the records are made of: source classes, and the measured mix once enough runs exist.","properties":{"classes":{"type":"array","items":{"type":"object","properties":{"class":{"type":"string"},"label":{"type":"string"}}}},"single":{"type":"boolean"},"measured":{"type":["object","null"]},"window":{"type":["string","null"]},"text":{"type":"string"}}},"filters":{"type":"array","items":{"type":"string"},"description":"Every active filter, one sentence each."},"cap":{"type":"string"},"schedule":{"type":"string"},"dedup":{"type":"string"},"destination":{"type":"string","description":"Where records go, with credentials removed."}}},"recent_changes":{"type":"array","items":{"type":"object"}},"last_run_at":{"type":["string","null"],"format":"date-time"},"next_run_at":{"type":["string","null"],"format":"date-time","description":"Null for feeds that run on request or on their own clock."},"manage_url":{"type":"string"}}}}}},"example":{"count":1,"feeds":[{"id":"996e114d-0fe5-414b-bb5d-9c075eb814b1","name":"Final expense intent, daily","active":false,"age_floor":null,"description":"Delivers audience people showing in-market web intent. 100% in-market web intent signals, no fill. No audience filters are applied, this feed delivers everyone its source returns. 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 www.talkdatatome.online, 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 www.talkdatatome.online, with your field mapping applied"},"recent_changes":[],"last_run_at":"2026-08-24T23:11:17.276669+00:00","next_run_at":null,"manage_url":"/data-feeds/996e114d-0fe5-414b-bb5d-9c075eb814b1"}]}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/feeds/change":{"post":{"tags":["Feeds"],"operationId":"changeFeed","summary":"Preview, then apply, a targeting change on a feed","description":"Two steps by contract. A call **without** `confirm: true` never changes anything: it returns\nthe measured volume impact, from the feed's own recent records, plus a `confirm_token`.\nApplying needs both `confirm: true` and that token, so a change cannot be one-shotted\nwithout somebody having seen the numbers. Tokens are good for about an hour.\n\nOnly the fields you can edit on the feed page move here. Today that is the age floor:\n`age_floor` (18 to 99) or `remove_age_floor: true`. Endpoints, credentials, and new fields\nstay person-only. An applied change is read back from the saved row before we say `verified`,\nand lands in the feed's change history as made by you. It takes effect on the next run.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"string","format":"uuid","description":"From `GET /api/v1/feeds`."},"age_floor":{"type":"integer","minimum":18,"maximum":99},"remove_age_floor":{"type":"boolean"},"confirm":{"type":"boolean","description":"True to apply. Needs `confirm_token` from the preview."},"confirm_token":{"type":"string"}},"required":["feed_id"]},"examples":{"preview":{"summary":"Step 1: preview","value":{"feed_id":"996e114d-0fe5-414b-bb5d-9c075eb814b1","age_floor":65}},"apply":{"summary":"Step 2: apply","value":{"feed_id":"996e114d-0fe5-414b-bb5d-9c075eb814b1","age_floor":65,"confirm":true,"confirm_token":"9f3a1c..."}}}}}},"responses":{"200":{"description":"Preview (`applied: false`) or applied change (`applied: true`).","content":{"application/json":{"schema":{"type":"object","properties":{"applied":{"type":"boolean"},"feed":{"type":"string"},"impact":{"type":"object","description":"Preview only. Current and proposed floor and the measured volume effect."},"already_satisfied":{"type":"boolean","description":"Preview only. True when the feed already has that floor; no token is issued."},"confirm_token":{"type":"string","description":"Preview only."},"next_step":{"type":"string","description":"Preview only."},"verified":{"type":"boolean","description":"Applied only. The saved row was read back and matches."},"age_floor_before":{"type":["integer","null"]},"age_floor_after":{"type":["integer","null"]},"takes_effect":{"type":"string","description":"Applied only. \"next run\"."},"description":{"type":["string","null"],"description":"Applied only. The feed described after the change."}},"required":["applied","feed"]}}}},"400":{"description":"Bad JSON, `feed_id` missing, or no change named (`age_floor` or `remove_age_floor`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No feed with that id on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`confirm_token` missing or stale. Preview again for a fresh one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The change did not save. Nothing changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/feeds/control":{"post":{"tags":["Feeds"],"operationId":"controlFeed","summary":"Pause, resume, or set the daily cap on a feed","description":"The same module the campaign chat uses, so pausing here means exactly what pausing there\nmeans. The account comes from the key, never from the body; a feed on another account answers 404.\n\nGoing live is owner-gated. Resuming a paused data feed files an approval for the account owner\ninstead of switching it on, and the response says so (`pending_approval: true`, `applied: false`)\nrather than claiming success. A daily cap above the plan ceiling is refused with 409 and nothing changes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"string","format":"uuid"},"action":{"type":"string","enum":["pause","resume","set_daily_cap"]},"daily_cap":{"type":"integer","minimum":1,"description":"Required for `set_daily_cap`."}},"required":["feed_id","action"]},"examples":{"pause":{"summary":"Pause","value":{"feed_id":"996e114d-0fe5-414b-bb5d-9c075eb814b1","action":"pause"}},"cap":{"summary":"Set a daily cap","value":{"feed_id":"996e114d-0fe5-414b-bb5d-9c075eb814b1","action":"set_daily_cap","daily_cap":2500}}}}}},"responses":{"200":{"description":"The result. Check `applied`; a no-op (already paused, same cap) answers `applied: false` with a message.","content":{"application/json":{"schema":{"type":"object","properties":{"applied":{"type":"boolean"},"pending_approval":{"type":"boolean"},"kind":{"type":"string","description":"Which kind of campaign the id resolved to."},"feed_id":{"type":"string"},"active":{"type":"boolean"},"daily_cap":{"type":"integer"},"message":{"type":"string"}},"required":["applied","pending_approval","feed_id","message"]}}}},"400":{"description":"Bad JSON, `feed_id` missing, `action` not one of pause, resume, set_daily_cap, or `daily_cap` missing or not a whole number above zero.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No campaign with that id on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The cap is above the plan ceiling. The body carries `next_step`. Nothing changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The change did not save or did not stick when read back.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/audience-delivery":{"get":{"tags":["Account"],"operationId":"audienceDelivery","summary":"What went out to each ad platform audience, per day","description":"Per audience, per day, how many records were delivered to the ad platform. Every number is\nrecords **net new** to that audience on that date: a person already on the list is never\nsent again, so repeats are never counted. Free and read-only; no PII is ever in the response.\n\nThe same module feeds the Overview page, so a number quoted here is the number on your screen.","parameters":[{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":365,"default":30},"description":"Rolling window."},{"name":"audience_id","in":"query","schema":{"type":"string"},"description":"Limit to one audience."},{"name":"include_daily","in":"query","schema":{"type":"boolean","default":true},"description":"Send `false` to drop the `per_day` series and get the summary only."}],"responses":{"200":{"description":"Delivery report.","content":{"application/json":{"schema":{"type":"object","properties":{"window":{"type":"object","properties":{"days":{"type":"integer"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"today_is_partial":{"type":"boolean"}}},"audiences":{"type":"array","items":{"type":"object","properties":{"audience_id":{"type":"string"},"name":{"type":"string"},"platform":{"type":"string"},"active":{"type":"boolean"},"sends_new_rows_daily":{"type":"boolean"},"cadence":{"type":"string"},"status":{"type":"string","description":"e.g. delivering, stopped."},"summary":{"type":"string","description":"One sentence a person can read."},"delivered_in_window":{"type":"integer"},"days_in_window":{"type":"integer"},"days_with_delivery":{"type":"integer"},"average_per_delivery_day":{"type":"integer"},"average_per_calendar_day":{"type":"integer"},"best_day":{"type":["object","null"],"properties":{"date":{"type":"string"},"records":{"type":"integer"}}},"last_delivery":{"type":["object","null"],"properties":{"date":{"type":"string"},"records":{"type":"integer"}}},"per_day":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"records":{"type":"integer"}}},"description":"Omitted when `include_daily=false`."}}}},"totals":{"type":"object","properties":{"audiences":{"type":"integer"},"delivering":{"type":"integer"},"delivered_in_window":{"type":"integer"}}},"unavailable":{"type":"boolean"},"counts_what":{"type":"string"}}},"example":{"window":{"days":30,"from":"2026-08-10","to":"2026-09-08","today_is_partial":true},"audiences":[{"audience_id":"a74f0fc8288c9e6f","name":"Roku cron path test","platform":"Roku","active":true,"sends_new_rows_daily":false,"cadence":"none","status":"stopped","summary":"Nothing new has gone out since Aug 16, when 9,000 new records went out.","delivered_in_window":9000,"days_in_window":30,"days_with_delivery":1,"average_per_delivery_day":9000,"average_per_calendar_day":300,"best_day":{"date":"2026-08-16","records":9000},"last_delivery":{"date":"2026-08-16","records":9000},"per_day":[{"date":"2026-08-15","records":0},{"date":"2026-08-16","records":9000},{"date":"2026-08-17","records":0}]}],"totals":{"audiences":1,"delivering":0,"delivered_in_window":9000},"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."}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`audience_id` does not match an audience on this account. Call without it to list them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ad-spend":{"get":{"tags":["Account"],"operationId":"adSpend","summary":"What your connected ad accounts spent","description":"The same rows our billing reads, so an invoice and this endpoint cannot disagree. Scoped to the\nad accounts connected under your account. Defaults to the last 30 days, grouped by day.\n\n### Two fields to read before reconciling against your own dashboard\n\n`settled` is false while the platform is still restating that day (some re-attribute for up to\n28 days, some true up invalid clicks for about 3, and the most recent day is usually partial).\nAn unsettled day is real but not final, and it is not what gets billed. A group is settled only\nif every day in it is.\n\n`locked` is the value frozen when a billing period closed. Once locked, `spend` may still drift as\nthe platform restates but `locked` never moves, because that is the number an invoice was built\nfrom. It is null until a period has closed.\n\nDates are the ad account's own reporting timezone, exactly as the platform reported them, so\nthey match the platform's own dashboard across month boundaries.","parameters":[{"name":"start","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD. Default 29 days before `end`."},{"name":"end","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD. Default today."},{"name":"group_by","in":"query","schema":{"type":"string","enum":["day","account","campaign","platform"],"default":"day"}},{"name":"platform","in":"query","schema":{"type":"string","enum":["meta","google_ads","vibe"]},"description":"Narrow to one platform."}],"responses":{"200":{"description":"Spend for the range.","content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string","format":"date"},"end":{"type":"string","format":"date"},"group_by":{"type":"string"},"platform":{"type":"string","description":"Echoed when you narrowed to one."},"total_spend":{"type":"number","description":"Dollars, two decimals."},"currency":{"type":"string","description":"Comma-joined when an account reports in more than one; totals across currencies are never summed silently."},"all_settled":{"type":"boolean","description":"Every day in the range is final and safe to reconcile against."},"days":{"type":"integer","description":"Underlying rows in the range."},"results":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"date":{"type":"string","description":"group_by=day."},"platform":{"type":"string","description":"Other groupings."},"ad_account_id":{"type":"string","description":"group_by=account or campaign."},"campaign_id":{"type":"string","description":"group_by=campaign."},"campaign_name":{"type":"string","description":"group_by=campaign."},"spend":{"type":"number"},"locked":{"type":["number","null"]},"settled":{"type":"boolean"},"impressions":{"type":"integer"},"clicks":{"type":"integer"},"currency":{"type":"string"}}}}}},"example":{"start":"2026-08-01","end":"2026-09-08","group_by":"day","total_spend":0,"currency":"","all_settled":true,"days":0,"results":[]}}}},"400":{"description":"`start` or `end` is not YYYY-MM-DD, `end` is before `start`, or `group_by` is unknown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Key is not yet linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Could not read spend right now. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/suppressions":{"get":{"tags":["Account"],"operationId":"suppressionsCount","summary":"How many numbers are on your do-not-contact list","responses":{"200":{"description":"Counts by how the numbers arrived.","content":{"application/json":{"schema":{"type":"object","properties":{"phones_uploaded":{"type":"integer","description":"From files uploaded in the app."},"phones_via_api":{"type":"integer","description":"Added through this endpoint."},"total":{"type":"integer"}}},"example":{"phones_uploaded":12480,"phones_via_api":212,"total":12692}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Account"],"operationId":"suppressionsAdd","summary":"Add numbers to your do-not-contact list","description":"Append phones from your own systems. A number posted here is excluded from every pull on the\naccount from the next one on, and blocked on the realtime API too, with no other step.\nAdditive only: nothing here ever removes a number. Up to 50,000 per call; ten-digit US numbers,\na leading 1 accepted. Repeats are ignored, so re-posting is safe.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"phones":{"type":"array","maxItems":50000,"items":{"type":"string"}}},"required":["phones"]},"examples":{"add":{"summary":"Add two numbers","value":{"phones":["4155550142","1 (212) 555-0188"]}}}}}},"responses":{"200":{"description":"Stored.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"received":{"type":"integer","description":"Valid, distinct numbers in this call."},"added":{"type":"integer","description":"How many were new."},"total_phones":{"type":"integer","description":"On the API list after this call."}}},"example":{"ok":true,"received":2,"added":2,"total_phones":214}}}},"400":{"description":"Bad JSON, no valid 10-digit US phone numbers in `phones`, or more than 50,000 in one call.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is not linked to an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The API list would exceed 500,000 numbers. Upload a file from the Suppression tab for lists that large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/revenue":{"post":{"tags":["Rev share"],"operationId":"reportRevenue","summary":"Report what a delivered list earned","description":"For buyers on a revenue share. Every list we deliver carries a `list_id` (on the file or in the\npayload). Post what you owe TDTM against it, as a total for a period (`reports`) or one sale at a\ntime (`transactions`). Both price identically; sales are better evidence. Up to 500 items per call.\n\n### Three rules\n\n1. **Your figure is never lost.** A report we cannot tie to a delivery is stored with\n   `match: \"unmatched\"` and returned with an explanation, not answered with a 400.\n2. **Nothing is invented.** Unmatched money is shown for exactly what it is.\n3. **Re-posting is safe.** A report is keyed on what it is about (list, period, your own\n   `external_id`), so a second post updates the row, bumps `revision`, and keeps the figure it replaced.\n\nSend both sales and the total they add up to and you are not counted twice: the sales win, and\nthe total is set aside and reported as such. Rows that fail validation are answered per row in\n`results`; the batch as a whole is 400 only when nothing in it could be stored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Send `reports`, `transactions`, or both.","properties":{"reports":{"type":"array","maxItems":500,"items":{"type":"object","description":"A total over a period for one list. `revenue` is dollars; send `revenue_cents` instead to work in whole cents.","properties":{"list_id":{"type":"string","description":"The id on the file or payload we sent you, e.g. FINALEXPEN-996E-20260824-222CFF7A. One list id is one delivery. An ad audience is reported by month as AUD-<feed8>-<YYYYMM>, the handle shown on the Revenue page and by the list_reportable_lists MCP tool. Required on every report."},"revenue":{"type":"number","description":"Gross revenue on those records, in dollars. A string with a currency symbol or thousands separators is also read."},"revenue_cents":{"type":"integer","description":"Alternative to `revenue`, in whole cents."},"period_start":{"type":"string","format":"date","description":"YYYY-MM-DD. Optional; the delivery date is known from the list_id."},"period_end":{"type":"string","format":"date","description":"YYYY-MM-DD. Optional."},"feed_code":{"type":"string","description":"Optional, alongside the list_id. It never replaces it: a report with a feed_code and no list_id is rejected."},"external_id":{"type":"string","description":"Your own reference (an invoice or report id). Re-sending the same one updates it; a different one is a new report."},"records_sold":{"type":"integer","description":"How many records converted. Explains the figure, never prices anything."},"records_used":{"type":"integer"},"currency":{"type":"string","description":"ISO code, default USD. Other currencies are stored but not mixed into USD totals."},"notes":{"type":"string"}},"required":["revenue"]}},"transactions":{"type":"array","maxItems":500,"items":{"type":"object","description":"One sale on one day. Better evidence than a total: a list of sales can be pointed at in a dispute.","properties":{"list_id":{"type":"string"},"revenue":{"type":"number","description":"Dollars. `revenue_cents` also accepted."},"occurred_on":{"type":"string","format":"date","description":"The day the sale happened, YYYY-MM-DD. Required."},"record_ref":{"type":"string","description":"Your reference for the record that earned. Never used to price."},"email":{"type":"string","description":"Optional. The email of the person who converted, or an md5, sha1 or sha256 of it (read by length). Used only to tie this sale to the exact record we delivered, which is what lets us build sharper audiences for you. Hashed on arrival and never stored as plaintext. NEVER used to price."},"phone":{"type":"string","description":"Optional. The phone of the person who converted, or a hash of the ten digit form. Any formatting. Same rules as email: linkage only, NEVER used to price."},"email_sha256":{"type":"string","description":"The same as sending the hash in `email`, said explicitly. `email_md5`, `phone_sha256` and `phone_md5` are read too. Normalise before hashing: lowercase the address, ten digits only for a phone."},"currency":{"type":"string"}},"required":["list_id","revenue","occurred_on"]}}}},"examples":{"total":{"summary":"A total for one list","value":{"reports":[{"list_id":"FINALEXPEN-996E-20260824-222CFF7A","revenue":4120}]}},"sales":{"summary":"Individual sales","value":{"transactions":[{"list_id":"FINALEXPEN-996E-20260824-222CFF7A","revenue":62.5,"occurred_on":"2026-09-14","record_ref":"lead-88"}]}},"identified":{"summary":"A sale, naming who converted","value":{"transactions":[{"list_id":"FINALEXPEN-996E-20260824-222CFF7A","revenue":62.5,"occurred_on":"2026-09-14","email":"jane.doe@example.com"}]}}}}}},"responses":{"200":{"description":"At least one item stored. Check each row in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"accepted":{"type":"integer"},"rejected":{"type":"integer"},"matched_to_a_delivery":{"type":"integer"},"transactions":{"type":"integer"},"period_totals":{"type":"integer"},"sales_tied_to_a_record":{"type":"integer","description":"Present only when the request identified somebody: how many sales were tied to a record on the delivery they named."},"note":{"type":"string","description":"Present when the request carried both sales and totals for the same list."},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Position in the array you sent."},"status":{"type":"string","enum":["stored","rejected"]},"error":{"type":"string","description":"Rejected rows only: why, in plain words."},"field":{"type":["string","null"],"description":"Rejected rows only: the field at fault."},"id":{"type":["string","null"]},"list_id":{"type":["string","null"]},"feed_code":{"type":["string","null"]},"kind":{"type":"string","enum":["period","transaction"]},"occurred_on":{"type":["string","null"]},"match":{"type":"string","description":"How the figure was tied to a delivery. `matched_run` means the list id resolved to the exact delivery; `unmatched` means the figure was stored and flagged for a person to reconcile."},"identity_match":{"type":"string","enum":["matched","matched_other_delivery","no_match","not_checked"],"description":"Only on a sale that carried an `email` or `phone`. Whether that person is a record on the delivery you quoted: `matched`, `matched_other_delivery` (we sent them on that feed, a different list), `no_match` (we compared the whole delivery and they are not in it), `not_checked` (we could not look, and will not pretend otherwise). Affects nothing about the money."},"revenue_cents":{"type":"integer"},"currency":{"type":"string"},"revision":{"type":"integer","description":"1 on first store; increments when you re-post the same report."},"replaced_revenue_cents":{"type":["integer","null"],"description":"The figure this revision replaced, kept for the audit trail."},"warnings":{"type":"array","items":{"type":"string"}}},"required":["index","status"]}}}},"example":{"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":[]}]}}}},"400":{"description":"Nothing could be stored: bad JSON, neither array present, or every item rejected (for example, no `list_id`). Per-item reasons are in `results`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"ok":{"type":"boolean"},"accepted":{"type":"integer"},"rejected":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Position in the array you sent."},"status":{"type":"string","enum":["stored","rejected"]},"error":{"type":"string","description":"Rejected rows only: why, in plain words."},"field":{"type":["string","null"],"description":"Rejected rows only: the field at fault."},"id":{"type":["string","null"]},"list_id":{"type":["string","null"]},"feed_code":{"type":["string","null"]},"kind":{"type":"string","enum":["period","transaction"]},"occurred_on":{"type":["string","null"]},"match":{"type":"string","description":"How the figure was tied to a delivery. `matched_run` means the list id resolved to the exact delivery; `unmatched` means the figure was stored and flagged for a person to reconcile."},"identity_match":{"type":"string","enum":["matched","matched_other_delivery","no_match","not_checked"],"description":"Only on a sale that carried an `email` or `phone`. Whether that person is a record on the delivery you quoted: `matched`, `matched_other_delivery` (we sent them on that feed, a different list), `no_match` (we compared the whole delivery and they are not in it), `not_checked` (we could not look, and will not pretend otherwise). Affects nothing about the money."},"revenue_cents":{"type":"integer"},"currency":{"type":"string"},"revision":{"type":"integer","description":"1 on first store; increments when you re-post the same report."},"replaced_revenue_cents":{"type":["integer","null"],"description":"The figure this revision replaced, kept for the audit trail."},"warnings":{"type":"array","items":{"type":"string"}}},"required":["index","status"]}}}},"example":{"ok":false,"accepted":0,"rejected":1,"results":[{"index":0,"status":"rejected","error":"Each report needs the list_id of the delivery it covers. It is the List_ID column on the file, the list_id field on the payload, and in the delivery email. A feed_code on its own is not enough.","field":"list_id"}]}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"More than 500 items in one request. Nothing was stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Revenue reporting is not switched on in this environment. Nothing was stored; retry later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Rev share"],"operationId":"readRevenue","summary":"Read back what you reported, or the list ids you can report on","description":"Default: your stored reports, newest first. `?mode=lists` instead returns the recent deliveries\nyou can report against, with their `list_id`, for when a file has been lost.","parameters":[{"name":"mode","in":"query","schema":{"type":"string","enum":["reports","lists"],"default":"reports"}},{"name":"list_id","in":"query","schema":{"type":"string"},"description":"reports mode. One list only."},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"reports mode. Updated at or after this time."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"Reports, or lists when `mode=lists`.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"count":{"type":"integer","description":"reports mode."},"total_reported_usd_cents":{"type":"integer","description":"reports mode. USD reports only."},"reports":{"type":"array","items":{"type":"object"},"description":"reports mode. Stored rows: list_id, feed_code, external_id, period, granularity, revenue_cents, currency, records_sold, match_status, revision, previous_revenue_cents, reported_at, updated_at."},"lists":{"type":"array","description":"lists mode.","items":{"type":"object","properties":{"list_id":{"type":"string"},"feed_code":{"type":["string","null"]},"feed":{"type":"string"},"delivered_at":{"type":"string","format":"date-time"},"records":{"type":"integer"},"status":{"type":"string"}}}},"note":{"type":"string","description":"lists mode, when no feeds are linked to this key yet."}}},"example":{"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":"failed"}]}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Revenue reporting is not switched on in this environment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activation/datasets/{id}/records":{"post":{"tags":["Activation"],"operationId":"activationLiveIntake","summary":"Stream new records into a connected database","description":"Point a website form, a CRM automation, or a Zapier webhook at this URL and every new sign-up\nlands in the connected database as it happens, matched the same way the batch loaders match.\n\nBody: one record, an array of records, or `{ \"records\": [...] }`, up to 500 per call. Field\nnames are detected the way CSV headers are (\"Email Address\", \"phone_number\", \"First Name\",\nzip or postal). Each record needs an email or a phone. Repeats and webhook retries are\ndeduplicated, never double-inserted.\n\n### Test mode\n\nAdd `?dry_run=1` (or `\"dry_run\": true` in the body) and we check the key, the database, and\nevery field name we can read out of your payload, then store nothing. It is the same code path\nright up to the insert, so a green dry run means the real call will work. The Test button on the\nDatabase Activation page is this call.\n\nForm tools that cannot set headers may pass the key as `?key=` instead.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The dataset id from the Database Activation page."},{"name":"dry_run","in":"query","schema":{"type":"string","enum":["1","true"]},"description":"Validate and store nothing."},{"name":"key","in":"query","schema":{"type":"string"},"description":"Your API key, only for tools that cannot set headers."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"One record."},{"type":"array","maxItems":500,"items":{"type":"object"}},{"type":"object","properties":{"records":{"type":"array","maxItems":500,"items":{"type":"object"}},"dry_run":{"type":"boolean"}}}]},"examples":{"one":{"summary":"One sign-up","value":{"email":"jane.doe@example.com","phone":"4155550142","first_name":"Jane","last_name":"Doe","zip":"94110"}},"many":{"summary":"A batch","value":{"records":[{"Email Address":"a@example.com"},{"phone_number":"2125550188","First Name":"Sam"}]}}}}}},"responses":{"200":{"description":"Stored (or, on a dry run, what would be stored).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"dry_run":{"type":"boolean","description":"Dry run only."},"dataset_id":{"type":"string"},"dataset_name":{"type":"string","description":"Dry run only."},"received":{"type":"integer"},"inserted":{"type":"integer","description":"Live only."},"matched":{"type":"integer","description":"Live only. Inserted rows matched to a person."},"duplicates":{"type":"integer","description":"Live only."},"invalid":{"type":"integer","description":"Live only."},"dataset":{"type":"object","description":"Live only.","properties":{"rows":{"type":"integer"},"matched":{"type":"integer"}}},"would_insert":{"type":"integer","description":"Dry run only."},"unusable":{"type":"integer","description":"Dry run only."},"records":{"type":"array","description":"Dry run only. What we read out of each record.","items":{"type":"object","properties":{"index":{"type":"integer"},"usable":{"type":"boolean"},"matched_on":{"type":["string","null"],"enum":["email","phone",null]},"read":{"type":"object"},"ignored":{"type":"array","items":{"type":"string"}},"dropped":{"type":"array","items":{"type":"string"}}}}},"note":{"type":"string","description":"Dry run only."}},"required":["ok"]},"example":{"ok":true,"dataset_id":"0d3b6a1e-2c4f-4e8a-9b7d-5f6e7a8b9c0d","received":2,"inserted":2,"matched":1,"duplicates":0,"invalid":0,"dataset":{"rows":1204,"matched":871}}}}},"400":{"description":"Bad JSON or no records in the body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Dataset not found for this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The dataset is matching a batch load right now, or is in an error state. Dry runs are not blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"More than 500 records in one call.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"No record had an email or phone, or the dataset is at its 1,000,000-row limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Activation"],"operationId":"activationLiveIntakeDescriptor","summary":"Usage instructions for the intake URL","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Dataset id."}],"responses":{"200":{"description":"Service name, version, and how to call POST."}}}},"/api/v1/activation/rank":{"post":{"tags":["Activation"],"operationId":"activationRank","summary":"Rank your own database and take the best records first","description":"Give one of your connected databases an offer and a target count, and get the best N records of\nYOUR OWN file back, ranked into five tiers with a score and the reason on every row.\n\n`mode: \"estimate\"` (the default) is free, stores nothing and charges nothing: it returns the tier\nbreakdown, how many of your records can be ranked at all, and the shortfall if your target is\nhigher than that. `mode: \"stage\"` writes the ranking down, mints the `list_id` that ties revenue\nback to the exact people in it, and holds the selection for release.\n\nNO RECORDS COME BACK THROUGH THIS ROUTE in either mode. The file is built and charged for only\nwhen the selection is released, and the download then appears on the Database Activation page.\n\n### The tiers, best first\n\nIn market now for your offer, recent interest in it, interest in a neighbouring market, reached out\nto a business like yours before, and household fit only. A record that matched a person but carries\nno signal at all is reported as `unscoreable` and never fills your count: nothing is ever padded to\nreach a target.\n\nA target that lands on a tie is broken by your own file order, and `tied_on_the_cut` says how many\nrecords sat on the cut score so you can raise the count and take them all.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate","stage"],"default":"estimate"},"database_name":{"type":"string","description":"Which of your databases, by the name you gave it."},"dataset_id":{"type":"string","description":"The database id, instead of the name."},"offer":{"type":"string","description":"What you are selling, in plain words. Omit to rank on household and property fit alone."},"target_count":{"type":"integer","description":"Required. How many records you want back, 1 to 100,000."},"live_window_days":{"type":"integer","description":"How recent \"in market now\" means, 1 to 30. Default 3."},"lookback_days":{"type":"integer","description":"How far back any interest still counts, 1 to 90. Default 30."},"ownership":{"type":"string","enum":["owner","renter","any"],"description":"Household preference for the fit score. Default any."},"income_floor":{"type":"integer","description":"Household income floor in dollars for a full fit score."},"age_min":{"type":"integer"},"age_max":{"type":"integer"}},"required":["target_count"]},"examples":{"estimate":{"summary":"What would I get","value":{"database_name":"Q3 customer list","offer":"solar","target_count":20000}},"stage":{"summary":"Order it","value":{"database_name":"Q3 customer list","offer":"solar","target_count":20000,"mode":"stage","ownership":"owner"}}}}}},"responses":{"200":{"description":"The ranking.","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["estimate","stage"]},"selection_id":{"type":["string","null"],"description":"Staged selections only."},"database":{"type":"string"},"offer":{"type":"string"},"state":{"type":"string","enum":["staged","released","error"]},"list_id":{"type":["string","null"],"description":"The handle for this selection, on every row of the file once it is released. Quote it to POST /api/v1/revenue."},"requested":{"type":"integer"},"selected":{"type":"integer","description":"What the file will hold. Never more than `requested`, and never padded."},"scoreable":{"type":"integer","description":"Matched records with anything to rank on. The ceiling on a selection."},"unscoreable":{"type":"integer","description":"Matched records we hold no signal on. Never selected, never charged for."},"matched":{"type":"integer"},"rows":{"type":"integer"},"shortfall":{"type":"integer","description":"Asked for minus delivered. Above zero means the file is short and says why."},"cut_score":{"type":["integer","null"]},"cut_tier":{"type":["string","null"]},"tied_on_the_cut":{"type":"integer"},"counts_cover_top_of_file_only":{"type":"boolean","description":"True on a database large enough that the tier counts cover its strongest part rather than every row."},"tiers":{"type":"array","description":"Every rankable record, by tier.","items":{"type":"object","properties":{"tier":{"type":"string"},"label":{"type":"string"},"blurb":{"type":"string"},"records":{"type":"integer"}}}},"tiers_in_the_file":{"type":"array","description":"What the selected records are made of.","items":{"type":"object","properties":{"tier":{"type":"string"},"label":{"type":"string"},"blurb":{"type":"string"},"records":{"type":"integer"}}}},"cost_usd":{"type":"number","description":"What the selection costs on release. Nothing is charged before that."},"summary":{"type":"string"},"next_step":{"type":"string"}}},"example":{"mode":"stage","selection_id":"a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d","database":"Q3 customer list","offer":"solar","state":"staged","list_id":"RANKED-3429-20260916-A1B2C3D4","requested":20000,"selected":20000,"scoreable":64358,"unscoreable":812004,"matched":876362,"rows":1000000,"shortfall":0,"cost_usd":400}}}},"400":{"description":"Bad JSON body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"No database of yours matched, the database is still matching, or no target count was given.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Activation"],"operationId":"activationRankList","summary":"The ranked selections on this account","parameters":[{"name":"dataset_id","in":"query","schema":{"type":"string"},"description":"Only the selections on one database."}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"selections":{"type":"array","items":{"type":"object","properties":{"selection_id":{"type":"string"},"database":{"type":"string"},"offer":{"type":"string"},"state":{"type":"string","enum":["staged","released","error"]},"list_id":{"type":["string","null"]},"selected":{"type":"integer"},"requested":{"type":"integer"},"shortfall":{"type":"integer"},"cost_usd":{"type":"number"},"summary":{"type":"string"},"created_at":{"type":["string","null"]}}}},"note":{"type":"string"}}}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/identify":{"post":{"tags":["Enrichment"],"operationId":"identify","deprecated":true,"summary":"Deprecated alias of enrich records mode","description":"**Deprecated.** This is the same name-gated waterfall as `POST /api/v1/enrich` with `records`,\nand returns the identical shape. It stays only so integrations built on it keep working; new\nintegrations should call `/api/v1/enrich`. For a single real-time phone, use `/api/v1/resolve`\nwith `verify`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","maxItems":100,"items":{"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"}}}}},"required":["records"]}}}},"responses":{"200":{"description":"Same shape as enrich records mode.","content":{"application/json":{"schema":{"type":"object","properties":{"requested":{"type":"integer"},"matched":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/RecordResult"}}}}}}},"400":{"description":"Malformed request: bad JSON, no identifiers, an unknown field group, or a batch over the cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Prepaid balance exhausted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Monthly record cap reached for this key. The body reports `monthly_row_cap` and `used_this_month`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/roofradar/pull":{"post":{"tags":["RoofRadar"],"operationId":"roofradarPull","summary":"Build a homeowner list for your territory","description":"ZIPs in, homeowner records out. Same territory, cap, and filters as the RoofRadar app. Needs a\nRoofRadar plan on the account (402 without one), and every ZIP must be inside the plan's\nterritory (403 otherwise, with the ZIPs that were outside). Up to 1,000 ZIPs per pull.\n\nReturns 202 while the list builds in the background. Poll `GET /api/v1/roofradar/pull` for\nstatus and the download link; the CSV is also emailed. Records count against the plan on the\n**first download**, not when the list is built. `mode: \"intent\"` (roofing intent) is an add-on.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"zips":{"type":"array","maxItems":1000,"items":{"type":"string"}},"max_records":{"type":"integer","minimum":1,"maximum":100000,"description":"Required: 1 to 100,000. Capped to what is left on the plan this month, with a warning."},"filters":{"type":"object","properties":{"built_on_or_before":{"type":"integer","description":"Year built ceiling. The usual roofing filter: older roofs first."},"built_on_or_after":{"type":"integer"},"min_home_value":{"type":"integer"},"max_home_value":{"type":"integer"},"min_income":{"type":"integer"},"min_owner_age":{"type":"integer"},"max_owner_age":{"type":"integer"},"phone":{"type":"string","enum":["mobile","any","none"],"description":"Which phone every row must carry. Default is what the app uses."},"require_email":{"type":"boolean"}}},"mode":{"type":"string","enum":["homeowners","intent"],"default":"homeowners"},"columns":{"type":"string","enum":["full","dialer"],"default":"full"}},"required":["zips","max_records"]},"examples":{"older":{"summary":"Older roofs in two ZIPs","value":{"zips":["49503","49504"],"max_records":500,"filters":{"built_on_or_before":2005,"phone":"mobile"}}}}}}},"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean"},"order_id":{"type":"string","format":"uuid"},"mode":{"type":"string"},"max_records":{"type":"integer"},"zips":{"type":"integer","description":"How many ZIPs were accepted."},"filters":{"type":"string","description":"The filters in one sentence."},"warnings":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}},"required":["queued","mode","max_records","zips"]},"example":{"queued":true,"mode":"homeowners","max_records":500,"zips":2,"warnings":[],"note":"Building now. Your list appears under Downloads and is emailed when ready; records count when you download."}}}},"400":{"description":"Bad body, no valid ZIPs, more than 1,000 ZIPs, `max_records` missing (max_records is required: 1 to 100,000), or a bad filter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"No RoofRadar plan on this account, the plan is waiting on payment or canceled, this month's records are used up, or roofing intent is not switched on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"One or more ZIPs are outside your territory. The body lists them in `outside`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["RoofRadar"],"operationId":"roofradarLists","summary":"Your recent lists, with download links","responses":{"200":{"description":"The last 50 lists, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"lists":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","description":"queued, running, completed, or failed."},"records":{"type":"integer"},"mode":{"type":"string","enum":["homeowners","intent"]},"summary":{"type":["string","null"],"description":"The filters, in one sentence."},"downloaded":{"type":"boolean","description":"True once the first download has metered the records."},"download_url":{"type":["string","null"],"description":"Present when the list is complete."},"ad_platform_url":{"type":["string","null"],"description":"The hashed file (SHA256 email and phone), present only after the list itself has been downloaded once."},"emailed_only":{"type":"boolean","description":"True when the file was emailed but is not stored for download."},"error":{"type":["string","null"]}}}},"records_remaining":{"type":"integer"},"period_end":{"type":["string","null"]}}}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"No RoofRadar plan on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/roofradar/resolve":{"post":{"tags":["RoofRadar"],"operationId":"roofradarResolve","summary":"One home, its roofing facts, its owner","description":"Send `{ address, zip }` or `{ phone }` and get the home's year built, roof age and cover,\nsize, value, sale history, and the owner with their best contacts. A match costs one record\nagainst the plan; a miss is free. The home's ZIP has to be inside your territory. Up to 60\nlookups a minute per key. Phones on your do-not-contact list are withheld and\n`suppressed_contact` is set.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string","description":"Street address. Needs `zip`."},"zip":{"type":"string","description":"Five digits."},"phone":{"type":"string","description":"Alternative to address plus zip."}}},"examples":{"address":{"summary":"By address","value":{"address":"745 Monroe Ave NW","zip":"49503"}},"phone":{"summary":"By phone","value":{"phone":"6165550100"}}}}}},"responses":{"200":{"description":"Lookup completed. Check `matched`.","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"boolean"},"matched_by":{"type":"string","enum":["address","phone"]},"billed":{"type":"boolean","description":"True on a match (one record against the plan). A miss is free."},"records_remaining":{"type":"integer"},"note":{"type":"string","description":"Present when the home was found but sits outside your territory."},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"county":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"owner_occupied":{"type":["boolean","null"]},"year_built":{"type":["integer","null"]},"roof_age_years":{"type":["integer","null"],"description":"Years since the home was built. A roof replaced since then will read older than it is."},"roof_cover":{"type":["string","null"],"description":"Roof material label when the code is known; null with the raw code in roof_cover_code otherwise, never a guess."},"roof_cover_code":{"type":["integer","null"]},"roof_shape_code":{"type":["integer","null"]},"stories":{"type":["number","null"]},"living_sqft":{"type":["integer","null"]},"home_value":{"type":["integer","null"]},"last_sale_date":{"type":["string","null"]},"last_sale_amount":{"type":["integer","null"]},"equity":{"type":["number","null"]},"mobile_home":{"type":["boolean","null"]},"suppressed_contact":{"type":"boolean","description":"Present and true when a phone was withheld because it is on your do-not-contact list."},"owner":{"type":["object","null"],"properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"mobile_phone":{"type":["string","null"]},"landline_phone":{"type":["string","null"]},"email":{"type":["string","null"]},"age":{"type":["integer","null"]},"household_income":{"type":["integer","null"]}}}},"required":["matched"]},"example":{"matched":true,"matched_by":"address","billed":true,"records_remaining":9871,"address":"745 MONROE AVE NW","city":"GRAND RAPIDS","state":"MI","zip":"49503","county":"KENT","year_built":1998,"roof_age_years":28,"roof_cover":"Asphalt shingle","roof_cover_code":3,"living_sqft":1840,"home_value":312000,"owner_occupied":true,"owner":{"first_name":"Jordan","last_name":"Avery","mobile_phone":"6165550100","landline_phone":null,"email":"jordan.avery@example.com","age":51,"household_income":92500}}}}},"400":{"description":"Bad JSON, or neither `address` plus `zip` nor `phone` was sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"No RoofRadar plan, or this month's records are used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The ZIP is outside your territory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 60 lookups in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Lookup failed. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/roofradar/lists/{id}/download":{"get":{"tags":["RoofRadar"],"operationId":"roofradarDownload","summary":"Download a finished list as CSV","description":"The first download meters the records against the plan; later downloads are free. Add\n`?format=hashed` for the ad platform file (SHA256 email and phone), which is only served after\nthe list itself has been downloaded once. When the remaining allotment is short the download is\nrefused with 402 rather than partially billed; the body carries `records` and `records_remaining`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The list id from `GET /api/v1/roofradar/pull`."},{"name":"format","in":"query","schema":{"type":"string","enum":["hashed"]},"description":"The hashed ad platform file instead of the list."}],"responses":{"200":{"description":"The CSV.","content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"The id is not a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, inactive, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"No RoofRadar plan, not enough records left on the plan for this list, or `format=hashed` before the first download.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such list on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The list has no file yet. Poll `GET /api/v1/roofradar/pull`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}