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.
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... ]}'{
"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"
}
]
}| Field | Type | Description |
|---|---|---|
record_id | string | Your stable ID for this record; echoed back in the response so you can join results. |
source_id | string | Your source tag for this record. Stored verbatim; per-source reporting and revenue rollups key off it. |
record_type | string | What 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. |
vertical | string | What 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_name | string, identity | First name. |
last_name | string, identity | Last name. |
address | string, identity | Street address. Optional when phone or email is present; those records match through the phone/email waterfall. |
city | string, identity | City. |
state | string, identity | 2-letter state. |
zip | string, identity | 5-digit ZIP. |
phone | string, identity | Your phone for the record. Used to resolve the person (phone hop of the waterfall); never echoed back. |
email | string, identity | Your 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_url | string | TrustedForm certificate URL, when the record has one. |
jornaya_lead_id | string | Jornaya LeadiD, when the record has one. |
opt_in_timestamp | string | Original opt-in timestamp, as your system recorded it. |
opt_in_url | string | The 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_address | string | IP 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_birth | string | Date of birth, in whatever format your system holds. Stored verbatim; not used for matching. |
gender | string | Gender, in whatever coding your system uses (M/F, Male/Female). Stored verbatim. |
credit_rating | string | Self-reported credit band from the original form (Excellent, Good, Fair, Poor). Stored verbatim; not a bureau score and never treated as one. |
homeowner_status | string | Whether the record is a homeowner, in your own coding (True/False, Own/Rent, Y/N). Stored verbatim. |
loan_amount | string | Original loan amount on the record, for mortgage verticals. Stored verbatim as sent; no currency symbol needed. |
loan_balance | string | Current loan balance on the record, for mortgage verticals. Stored verbatim. |
loan_to_value | string | Loan-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_rate | string | Interest rate on the current loan, in percent. Stored verbatim. |
loan_type | string | Loan type (Conventional, FHA, VA, Fixed, ...). Stored verbatim in your coding. |
loan_purpose | string | What the consumer was after (purchase, refinance, cash_out, ...). Stored verbatim. |
property_value | string | Property value as your source states it. Stored verbatim; no currency symbol needed. |
property_type | string | Property type (SINGLE_FAM, CONDO, Townhome, ...). Stored verbatim in your coding. |
vehicle_year | string | Vehicle year, for auto verticals. |
vehicle_make | string | Vehicle make, for auto verticals. |
vehicle_model | string | Vehicle model, for auto verticals. |
vin | string | VIN, 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
{
"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 }
]
}| Field | Type | Description |
|---|---|---|
received | number | Records in the batch. |
matched | number | Records that resolved to a person with an opt-in email. |
rejected | number | Records rejected as empty (no identity signal at all). 0 for healthy batches. |
warnings | string[] | 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. |
results | array | One entry per record, in the order you sent them, with your record_id echoed back. |
results[].matched | boolean | Whether this record resolved to a person with an opt-in email. |
results[].matched_by | string | Which identifier confirmed the person: "phone", "email", or "name_address". |
results[].email | string | The appended opt-in email, with its evidence fields (opt_in_ip, opt_in_url, opt_in_date). Only on matches. |
results[].rejected | boolean | True 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_backfill | boolean | True 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_backfill | object | Present 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.
| Field | Type | Description |
|---|---|---|
401 | auth | Missing, invalid, inactive, or expired API key. |
400 | request | Empty records array, more than 5000 records, invalid JSON, or a batch where every record arrived empty (the error lists the expected field names). |
500 | server | Append 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. |
503 | server | We 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.
{
"validate": true,
"records": [ ...a few sample records... ]
}{
"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.
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" }
]}'{
"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.
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.
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