Skip to content

Partner Data Feed

Send us records. Get receipts, matches, and per-source reporting.

This page is for data partners delivering records to TDTM. Two ways in: a JSON API for ongoing sends, and bulk CSV drops for historical volume. Every batch gets an itemized response, and the status endpoint answers "did it land?" without an email thread.

Authentication

Every API request carries your key in the X-API-Key header. Keys are issued by TDTM, scoped to your organization, and revocable. Keep them server-side.

Send records

POST batches of up to 5,000 records. Field names are case-sensitive and all lowercase. Send every field you have; anything your source does not carry can be an empty string or omitted.

POST /api/partner/auto-feed (up to 5,000 records per batch)
curl -X POST https://www.talkdatatome.online/api/partner/auto-feed \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"records": [ ...up to 5000 records... ]}'
Request body
{
  "records": [
    {
      "record_id": "lead-000123",
      "source_id": "MY_SOURCE_1",
      "record_type": "call",
      "vertical": "mortgage",
      "first_name": "Jane",
      "last_name": "Sample",
      "address": "123 Main St",
      "city": "Denver",
      "state": "CO",
      "zip": "80202",
      "phone": "3035551234",
      "email": "jane.sample@example.com",
      "trusted_form_cert_url": "https://cert.trustedform.com/abc123...",
      "jornaya_lead_id": "11111111-2222-3333-4444-555555555555",
      "opt_in_timestamp": "2026-01-08 13:45",
      "opt_in_url": "https://example.com/quote-form",
      "ip_address": "69.178.0.16",
      "date_of_birth": "1971-04-22",
      "gender": "F",
      "credit_rating": "GOOD",
      "homeowner_status": "True",
      "loan_amount": "296000.00",
      "loan_balance": "305000.00",
      "loan_to_value": "81.30",
      "interest_rate": "6.75",
      "loan_type": "Conventional",
      "loan_purpose": "refinance",
      "property_value": "370000.00",
      "property_type": "SINGLE_FAM",
      "vehicle_year": "2021",
      "vehicle_make": "HONDA",
      "vehicle_model": "CIVIC",
      "vin": "1HGCM82633A004352"
    }
  ]
}
FieldTypeDescription
record_idstringYour stable ID for this record; echoed back in the response so you can join results.
source_idstringYour source tag for this record. Stored verbatim; per-source reporting and revenue rollups key off it.
record_typestringWhat kind of record this is: "call" for a real inbound phone call the consumer placed, "form" for a web form fill. Absent means form, so nothing changes for a form-fill source. Send "call" on every inbound-call record: callers are never mixed into a form-fill or aged opt-in file, and a caller carrying no TrustedForm or Jornaya certificate is expected rather than a missing consent cert.
verticalstringWhat the record actually is (auto_insurance, auto_warranty, mortgage, solar, home_services, ...). Send it on every record: routing reads THIS, not source_id, and a record with no vertical is held rather than routed.
first_namestring, identityFirst name.
last_namestring, identityLast name.
addressstring, identityStreet address. Optional when phone or email is present; those records match through the phone/email waterfall.
citystring, identityCity.
statestring, identity2-letter state.
zipstring, identity5-digit ZIP.
phonestring, identityYour phone for the record. Used to resolve the person (phone hop of the waterfall); never echoed back.
emailstring, identityYour email for the record. Used to resolve the person (email hop of the waterfall); kept separate from the appended opt-in email we return.
trusted_form_cert_urlstringTrustedForm certificate URL, when the record has one.
jornaya_lead_idstringJornaya LeadiD, when the record has one.
opt_in_timestampstringOriginal opt-in timestamp, as your system recorded it.
opt_in_urlstringThe page URL the consumer filled the form on. Send it on the lead, or later as a URL-only record (record_id + opt_in_url, plus source_id when your ids repeat across sources) to backfill leads already delivered; a URL-only record updates the existing lead and never creates a new one.
ip_addressstringIP address captured at opt-in, when the record has one. Stored verbatim alongside the record; kept separate from the opt-in IP we append.
date_of_birthstringDate of birth, in whatever format your system holds. Stored verbatim; not used for matching.
genderstringGender, in whatever coding your system uses (M/F, Male/Female). Stored verbatim.
credit_ratingstringSelf-reported credit band from the original form (Excellent, Good, Fair, Poor). Stored verbatim; not a bureau score and never treated as one.
homeowner_statusstringWhether the record is a homeowner, in your own coding (True/False, Own/Rent, Y/N). Stored verbatim.
loan_amountstringOriginal loan amount on the record, for mortgage verticals. Stored verbatim as sent; no currency symbol needed.
loan_balancestringCurrent loan balance on the record, for mortgage verticals. Stored verbatim.
loan_to_valuestringLoan-to-value as your source states it, in percent. Stored verbatim; we do not recompute it and we do not assume it is current.
interest_ratestringInterest rate on the current loan, in percent. Stored verbatim.
loan_typestringLoan type (Conventional, FHA, VA, Fixed, ...). Stored verbatim in your coding.
loan_purposestringWhat the consumer was after (purchase, refinance, cash_out, ...). Stored verbatim.
property_valuestringProperty value as your source states it. Stored verbatim; no currency symbol needed.
property_typestringProperty type (SINGLE_FAM, CONDO, Townhome, ...). Stored verbatim in your coding.
vehicle_yearstringVehicle year, for auto verticals.
vehicle_makestringVehicle make, for auto verticals.
vehicle_modelstringVehicle model, for auto verticals.
vinstringVIN, for auto verticals.

Identity fields drive matching, which waterfalls phone, then email, then name plus address (every hop name-gated). Records with no street address still match through phone or email. source_id is stored verbatim on every record and drives per-source reporting and revenue rollups, so keep it stable per source.

Response

200 response
{
  "received": 2,
  "matched": 1,
  "rejected": 0,
  "results": [
    {
      "record_id": "81767C0D-8D64-CDCE-78C2-B75348EA3BE9",
      "matched": true,
      "matched_by": "phone",
      "email": "appended-opt-in@example.com",
      "opt_in_ip": "69.178.0.16",
      "opt_in_url": "https://example.com/form",
      "opt_in_date": "2026-01-08"
    },
    { "record_id": "second-record-id", "matched": false }
  ]
}
FieldTypeDescription
receivednumberRecords in the batch.
matchednumberRecords that resolved to a person with an opt-in email.
rejectednumberRecords rejected as empty (no identity signal at all). 0 for healthy batches.
warningsstring[]Present when something looks off: unknown field names (with the field we think you meant), fields that never carried a value, rejected-record counts. An empty template mistake shows up here on the first batch.
resultsarrayOne entry per record, in the order you sent them, with your record_id echoed back.
results[].matchedbooleanWhether this record resolved to a person with an opt-in email.
results[].matched_bystringWhich identifier confirmed the person: "phone", "email", or "name_address".
results[].emailstringThe appended opt-in email, with its evidence fields (opt_in_ip, opt_in_url, opt_in_date). Only on matches.
results[].rejectedbooleanTrue when the record was rejected, with a "reason": "empty_record", or for URL-only records "unknown_record_id", "ambiguous_record_id" or "lookup_failed".
results[].url_backfillbooleanTrue when the record was a URL-only record (record_id + opt_in_url). "stored" says whether the URL was attached, "source_id" which source it was placed under, and "matched_existing" whether we already hold that lead.
url_backfillobjectPresent when the batch carried URL-only records: received, stored, matched_existing, unknown_record_id, ambiguous_record_id, lookup_failed. These records update leads already delivered and are never counted as new leads.

A 200 means the batch was accepted, not that everything matched. Check matched, rejected, and warnings on every response; a batch of real records that comes back with zero matches and a warnings array almost always means a field-name mismatch on the sending side.

FieldTypeDescription
401authMissing, invalid, inactive, or expired API key.
400requestEmpty records array, more than 5000 records, invalid JSON, or a batch where every record arrived empty (the error lists the expected field names).
500serverAppend lookup failed, or we could not store the batch and the same records will fail again until we fix our side (body carries code "retention_failed", retryable false). Do not resend a retryable:false batch; we are alerted and will confirm.
503serverWe could not store your batch, so nothing was kept, counted, matched or billed (body carries code "retention_failed", retryable true, retry_after_seconds, and the store’s own error). Your file is fine. Resend it unchanged after Retry-After.

Test your template first (dry run)

Add "validate": true to any request and we parse it exactly like a real batch but store and match nothing. The response shows how many values we read per field and flags unknown field names with the field we think you meant. One call confirms a new export template before you send millions of records through it.

Request
{
  "validate": true,
  "records": [ ...a few sample records... ]
}
Response
{
  "validate": true,
  "received": 3,
  "would_reject_as_empty": 0,
  "field_coverage": { "first_name": 3, "phone": 3, "email": 2, "vin": 0, ... },
  "warnings": [
    "Unknown field \"PhoneNumber\" was ignored. Did you mean \"phone\"? ..."
  ],
  "note": "Dry run: nothing was stored or matched. Send without \"validate\" to deliver."
}

Send page URLs later (URL backfill)

If your source starts carrying the page URL the consumer filled the form on after leads were already delivered, send just the ids and the URLs to the same endpoint. A record with record_id and opt_in_url and no name, address, phone or email is read as an update to the lead you already sent: it is attached to that lead, it never creates a new lead, and it is never counted as a delivered record. Send source_id as well whenever your ids repeat across sources; an id we hold under more than one source is refused as ambiguous_record_id rather than guessed. The same shape works as a bulk file: a CSV whose only columns are the id, the URL and optionally the source id.

Request
curl -X POST https://www.talkdatatome.online/api/partner/auto-feed \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_KEY" \
  -d '{"records": [
    { "record_id": "81767C0D-8D64-CDCE-78C2-B75348EA3BE9", "source_id": "MY_SOURCE_1", "opt_in_url": "https://example.com/quote-form" },
    { "record_id": "second-record-id", "source_id": "MY_SOURCE_1", "opt_in_url": "https://example.com/other-form" }
  ]}'
Response
{
  "received": 2,
  "matched": 0,
  "rejected": 0,
  "url_backfill": { "received": 2, "stored": 2, "matched_existing": 2, "unknown_record_id": 0, "ambiguous_record_id": 0, "lookup_failed": 0 },
  "results": [
    { "record_id": "81767C0D-8D64-CDCE-78C2-B75348EA3BE9", "matched": false, "url_backfill": true, "stored": true, "source_id": "MY_SOURCE_1", "matched_existing": true },
    { "record_id": "second-record-id", "matched": false, "url_backfill": true, "stored": true, "source_id": "MY_SOURCE_1", "matched_existing": true }
  ]
}

Check what we received

The status endpoint returns, for your key: batches and rows received per day per source_id, field coverage (including how many rows carry your page URL), match rates, and a url_backfill block counting the URLs attached by URL-only sends, covering the last 30 days, plus bulk files received and loaded if you deliver files. Poll it after a send instead of asking.

GET /api/partner/feed-status
curl https://www.talkdatatome.online/api/partner/feed-status \
  -H "X-API-Key: YOUR_KEY"

Bulk file drops

For historical volume, drop CSV files on the SFTP we set up with you. Files are mirrored, header-validated, and loaded automatically, and a receipt goes out per file. Conventions that make this fully hands-off:

  • Use the agreed column template exactly (below). A file whose header differs is not loaded; you get a report of what differed instead.
  • Put the record count in the filename, like (107652 records). We verify loaded rows against it and flag any difference.
  • Send incremental files, not cumulative re-exports. We dedupe on your lead ID either way, but incremental keeps counts honest on both sides.
File template columns, in order
LeadID,source_id,FirstName,MiddleName,LastName,BirthDate,Gender,MaritalStatus,MailAddress1,MailAddress2,CityName,ProvinceStateName,PostalZipCode,County,PhoneNumber,EmailAddress,JornayaID,TrustedForm,LeadIPAddress,LeadDate,HomeownerStatus,Income,LoanAmount,LoanBalance,LoanType,LoanPurpose,PropertyValue,PropertyType,FHALoan,VALoan,VAStatus,CashOutAmount,Equity,LoanToValue,CreditRating,HomeCurrentlyInsured,HomeCurrentCarrier,HomePropertyType,YearBuilt,VehicleYear,VehicleMake,VehicleModel,Vertical