Reference
API Documentation
Programmatic access to skip tracing, DNC scrubbing and nationwide property data over HTTPS.
llms.txt — for AI coding assistantsBase URL
https://api.titanskip.com/v1Authentication
curl https://api.titanskip.com/v1/traces \
-H "Authorization: Bearer tsk_live_your_key_here" \
-H "Content-Type: application/json"Never expose your key in client-side code. Keep it secret and secure to prevent unauthorized access. Rotate keys immediately if you suspect compromise.
Get account info
https://api.titanskip.com/v1/account120 / minuteReturns the authenticated user's account info: credits balance, billing mode, and subscription/metered state. Use this to show a 'credits remaining' indicator or to pre-flight a submission before calling POST /traces.
| Parameter | Type | Required | Description |
|---|
GET /account{
"id": "9fa9e14b-6187-473c-8b5f-0b097823886f",
"email": "[email protected]",
"billing_mode": "prepaid",
"credits": 12450,
"subscription": {
"plan_id": "starter_v1",
"plan_name": "Starter",
"credits_limit": 25000,
"credits_used": 12550,
"current_period_end": "2026-06-01T00:00:00.000Z"
},
"metered": null
}Get pricing
https://api.titanskip.com/v1/pricing120 / minuteReturns the current per-row credit rates for each job type. Rates can change — always fetch before computing cost estimates client-side. These are your account's rates — the exact numbers every charge uses (all of an account's keys share them).
| Parameter | Type | Required | Description |
|---|
GET /pricing{
"rates": {
"normal": 1,
"enhanced": 2,
"llc": 3,
"dnc": 1,
"property": 2
}
}List trace jobs
https://api.titanskip.com/v1/traces1,200 / minuteReturns a paginated list of trace jobs for the authenticated user. Includes pagination metadata and a downloadable CSV URL for each trace.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | optional | Page number to fetch. Default: 1. |
limit | integer | optional | Records per page. Default: 10. Maximum: 100. |
type | string | optional | Filter by trace type: 'bulk' (CSV uploads) or 'single' (synchronous lookups). |
GET /traces?page=1&limit=10{
"_metadata": {
"page": 1,
"per_page": 10,
"page_count": 3,
"total_count": 24
},
"traces": [
{
"id": "trc_19568a12b3c7d4e8f5a6b7c8d9e",
"status": "completed",
"mode": "normal",
"include_llcs": true,
"total_rows": 1000,
"entity_rows": 40,
"match_count": 870,
"no_match_count": 110,
"error_count": 20,
"credits_charged": 1120,
"credits_refunded": 130,
"name": "owners",
"download_url": "https://api.titanskip.com/v1/traces/trc_19568a12b3c7d4e8f5a6b7c8d9e/download",
"created_at": "2026-03-17T16:00:00.000Z",
"completed_at": "2026-03-17T16:03:42.000Z"
}
]
}Create a trace job
https://api.titanskip.com/v1/traces120 / minuteUploads a CSV for bulk skip tracing and starts an async job. Provide a column_mapping pointing our keys at your CSV headers. Requirements depend on mode: normal needs first_name + last_name, enhanced ignores names. Every job needs at least one full address group (property OR mailing), and address groups are all-or-nothing — map all four fields of a group or none. Credits are charged up front per billable row and refunded for no-matches/errors after completion.
Field requirements
normalRequires first_name + last_name, plus one full address group (property OR mailing).
enhancedRequires one full address group (property OR mailing). Names are ignored.
Address groups are all-or-nothing: include all four fields of a group, or none. Every job needs at least one complete group.
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | required | CSV file of records to trace (multipart/form-data). Max 50 MB. |
mode | string | optional | `normal` (default) — name + address lookup; requires first_name + last_name. `enhanced` — address-only deeper lookup; names are ignored. Either way, at least one full address group is required. |
column_mapping | JSON string | required | Maps our field keys to your CSV header names — e.g. {"first_name":"First Name"}. See the per-key badges: names are required in normal mode; each address group is all-or-nothing and at least one full group (property or mailing) is required. |
include_llcs | boolean | optional | Also trace company/LLC rows. In `normal` mode they run as a parallel sub-job billed at the LLC rate; in `enhanced` mode they're folded into the main job. Default: `false`. |
curl -X POST https://api.titanskip.com/v1/traces \
-H "Authorization: Bearer tsk_live_your_key_here" \
-F "[email protected]" \
-F "mode=normal" \
-F "include_llcs=true" \
-F 'column_mapping={
"first_name": "First Name",
"last_name": "Last Name",
"property_street": "Property Address",
"property_city": "Property City",
"property_state": "Property State",
"property_zip": "Property Zip"
}'{
"success": true,
"id": "trc_19568a12b3c7d4e8f5a6b7c8d9e",
"status": "processing",
"mode": "normal",
"include_llcs": true,
"total_rows": 1000,
"entity_rows": 40,
"cost": 1120,
"credits_charged": 1120,
"columns": {
"first_name": "First Name",
"last_name": "Last Name",
"property_street": "Property Address",
"property_city": "Property City",
"property_state": "Property State",
"property_zip": "Property Zip"
}
}Validation errors
400A rejected mapping returns HTTP 400 with a human-readable error and a details array listing every problem at once — so you can fix them all in one pass. Unknown keys include a “did you mean” hint.
{
"error": "Normal mode is missing required fields: \"first_name\", \"last_name\". At least one full address group is required: map all of property_street/city/state/zip, or all of mailing_street/city/state/zip.",
"details": [
"Normal mode is missing required fields: \"first_name\", \"last_name\".",
"At least one full address group is required: map all of property_street/city/state/zip, or all of mailing_street/city/state/zip."
]
}{
"error": "Unknown field \"address\" — did you mean \"property_street\"?",
"details": [
"Unknown field \"address\" — did you mean \"property_street\"?"
]
}{
"error": "Property address is all-or-nothing — you mapped \"property_street\", \"property_city\", \"property_state\" but are missing \"property_zip\".",
"details": [
"Property address is all-or-nothing — you mapped \"property_street\", \"property_city\", \"property_state\" but are missing \"property_zip\"."
]
}{
"error": "These mapped columns are not in the CSV: \"Street Addr\" (mapped to property_street). Available headers: First Name, Last Name, Address, City, State, Zip.",
"details": [
"These mapped columns are not in the CSV: \"Street Addr\" (mapped to property_street). Available headers: First Name, Last Name, Address, City, State, Zip."
]
}Get a trace job
https://api.titanskip.com/v1/traces/:id1,200 / minuteReturns a single trace job by ID. The shape matches each item in GET /traces and the trace.* webhook payload's data field, so the same parser works for polling and webhooks.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Trace job ID (prefixed trc_). |
GET /traces/trc_19568a12b3c7d4e8f5a6b7c8d9e{
"id": "trc_19568a12b3c7d4e8f5a6b7c8d9e",
"status": "completed",
"mode": "normal",
"include_llcs": true,
"total_rows": 1000,
"entity_rows": 40,
"match_count": 870,
"no_match_count": 110,
"error_count": 20,
"credits_charged": 1080,
"credits_refunded": 154,
"name": "owners",
"download_url": "https://api.titanskip.com/v1/traces/trc_19568a12b3c7d4e8f5a6b7c8d9e/download",
"created_at": "2026-03-17T16:00:00.000Z",
"completed_at": "2026-03-17T16:03:42.000Z",
"breakdown": {
"individuals": { "rate": 1, "rows": 960, "match": 842, "no_match": 100, "error": 18, "credits_charged": 960, "credits_refunded": 118 },
"llcs": { "rate": 3, "rows": 40, "match": 28, "no_match": 10, "error": 2, "credits_charged": 120, "credits_refunded": 36 }
}
}Single record trace (synchronous)
https://api.titanskip.com/v1/traces/single120 / minuteSkip-trace a single record and get the result back inline — no CSV, no job, no webhook. Charges 1 credit at the normal rate (or the LLC rate if `is_entity` is true). Use for one-off lookups or manual research from your own app.
| Parameter | Type | Required | Description |
|---|---|---|---|
first_name | string | required | Owner first name, OR full company / LLC name (e.g. "ACME Holdings LLC"). |
last_name | string | optional | Owner last name. Leave blank for companies. |
address | string | required | Property street address. |
city | string | required | Property city. |
state | string | required | Property state (2-letter code). |
zip | string | required | Property zip / postal code. |
mailing_address | string | optional | Mailing street address. Optional but improves match quality. |
mailing_city | string | optional | Mailing city. Required if `mailing_address` is set. |
mailing_state | string | optional | Mailing state. Required if `mailing_address` is set. |
mailing_zip | string | optional | Mailing zip. Required if `mailing_address` is set. |
is_entity | boolean | optional | Force LLC pricing. Auto-detected if `first_name` contains a company suffix (LLC, INC, CORP, TRUST, etc.). Default: `false`. |
POST /traces/single
Content-Type: application/json
{
"first_name": "Jordan",
"last_name": "Reeves",
"address": "1842 Oak Ridge Dr",
"city": "Phoenix",
"state": "AZ",
"zip": "85032"
}{
"matched": true,
"first_name": "Jordan",
"last_name": "Reeves",
"phones": [
{ "number": "+1 (602) 555-0182", "type": "mobile" },
{ "number": "+1 (480) 555-9921", "type": "landline" }
],
"emails": ["[email protected]"],
"credits": 1,
"credits_refunded": 0,
"is_entity": false
}List DNC jobs
https://api.titanskip.com/v1/dnc1,200 / minuteReturns a paginated list of DNC + TCPA jobs for the authenticated user. Each job represents one uploaded phone list scrubbed against the Do Not Call registry and the TCPA litigator list.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | optional | Page number to fetch. Default: 1. |
limit | integer | optional | Records per page. Default: 10. Maximum: 100. |
GET /dnc?page=1&limit=10{
"_metadata": {
"page": 1,
"per_page": 10,
"page_count": 2,
"total_count": 14
},
"jobs": [
{
"id": "dnc_19568a12b3c7d4e8f5a6b7c8d9e",
"name": "Lead list - May 20 2026",
"status": "completed",
"total_rows": 5000,
"match_count": 4980,
"error_count": 20,
"credits_charged": 5000,
"credits_refunded": 20,
"download_url": "https://api.titanskip.com/v1/dnc/dnc_19568a12b3c7d4e8f5a6b7c8d9e/download",
"created_at": "2026-05-20T14:30:00.000Z",
"completed_at": "2026-05-20T14:33:12.000Z"
}
]
}Create a DNC job
https://api.titanskip.com/v1/dnc120 / minuteUploads a CSV containing phone numbers and scrubs them against the Do Not Call registry and the TCPA litigator list. Billed per phone at the dnc rate (see GET /pricing). Returns immediately with the new job ID — completion is asynchronous (webhook or poll).
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | required | CSV file containing one phone number per row. Max size: 50 MB. |
column_mapping | string | required | JSON string mapping our 'phone' key to your CSV column header — e.g. {"phone":"Phone Number"}. Numbers can be in any format; we normalize before sending upstream. |
POST /dnc
Content-Type: multipart/form-data
file=phones.csv
column_mapping={"phone":"Phone Number"}{
"id": "dnc_19568a12b3c7d4e8f5a6b7c8d9e",
"name": "phones",
"status": "processing",
"total_rows": 5000,
"rate": 1,
"credits_charged": 5000
}Get a DNC job
https://api.titanskip.com/v1/dnc/:id1,200 / minuteReturns a single DNC job by ID for the authenticated user. The shape matches the webhook payload's data field, so the same parser works for both polling and webhook receipt.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | DNC job ID (prefixed dnc_). |
GET /dnc/dnc_19568a12b3c7d4e8f5a6b7c8d9e{
"id": "dnc_19568a12b3c7d4e8f5a6b7c8d9e",
"status": "completed",
"total_rows": 5000,
"match_count": 4980,
"error_count": 20,
"rate": 1,
"credits_charged": 5000,
"credits_refunded": 20,
"download_url": "https://api.titanskip.com/v1/dnc/dnc_19568a12b3c7d4e8f5a6b7c8d9e/download",
"created_at": "2026-05-20T14:30:00.000Z",
"completed_at": "2026-05-20T14:33:12.000Z",
"error_message": null
}Property data
Nationwide property search, map pins, and full property records.
Searching and mapping are free. You only spend credits when you buy a property's full record — one at a time with POST /properties/lookup, or in bulk with POST /property-exports. A pin's id is the property ID both of them take.
Free
POST /properties/pins— every matching property as id + coordinatesPOST /properties/count— how many matchPOST /properties/autocomplete— search-box suggestions- Re-reading a property or export you already bought
Uses credits — rates.property each
POST /properties/lookup— one full record, returned inlinePOST /property-exports— up to 10,000 records, async with a webhook- Charged up front; properties with no record are refunded
- Check the current rate with
GET /pricing
| Limit | Value | Details |
|---|---|---|
| Map pins per account | 5,000,000 / day | Resets 00:00 UTC. X-Pins-Daily-Limit / X-Pins-Daily-Remaining on every pins response; pages shrink to what's left, then 429 DAILY_PIN_LIMIT. |
| Pins, count, suggestions | 600 / min per key | Plus 1,200 / min per account, shared by all of its keys. |
| Property lookups | 120 / min per key | Plus 240 / min per account. |
| New property exports | 10 / min per key | Plus 20 / min per account, and at most 3 processing at once. |
| Pins per page | 10,000 | Page with offset / next_offset. |
| IDs per export | 10,000 | Duplicates removed before charging. |
Building a map
Lots of people panning your map at once is expected — the limits are sized for it, as long as each map fetches like this (it's how the TitanSkip dashboard's own map works):
Fetch on moveend, not move
One request per pan or zoom gesture, not one per animation frame.
Debounce ~400 ms
Wait for the map to settle, and abort the previous request when a new one starts.
Snap the view to a grid
Round the bounds outward (e.g. to 25% of the view) so small pans repeat the exact same query.
Cache by query
Keep responses for a few minutes, keyed by snapped bounds + filters, and skip the request on a hit.
Count when zoomed out
Below about zoom 10 (metro scale), call /properties/count and ask the user to zoom in.
Keep the key on your server
Browsers call your backend; your backend calls TitanSkip. Never ship the key to a client.
// In the browser. Calls YOUR backend (below), never TitanSkip directly.
const DEBOUNCE_MS = 400; // wait for the map to settle
const SNAP = 0.25; // round the view outward to 25% of its size
const MIN_PIN_ZOOM = 10; // below this, show a count instead of pins
const cache = new Map(); // query → { at, pins }
let timer, controller;
map.on("moveend", () => { // once per gesture — never on "move"
clearTimeout(timer);
timer = setTimeout(loadPins, DEBOUNCE_MS);
});
function snap(b) {
const h = (b.north - b.south) * SNAP;
const w = (b.east - b.west) * SNAP;
return {
south: Math.floor(b.south / h) * h, north: Math.ceil(b.north / h) * h,
west: Math.floor(b.west / w) * w, east: Math.ceil(b.east / w) * w,
};
}
async function loadPins() {
const bounds = snap(currentBounds(map)); // { north, south, east, west }
const path = map.getZoom() < MIN_PIN_ZOOM ? "count" : "pins";
const key = JSON.stringify({ path, bounds, filters });
const hit = cache.get(key); // small pans → same key
if (hit && Date.now() - hit.at < 5 * 60_000) return render(path, hit.body);
controller?.abort(); // drop the stale request
controller = new AbortController();
try {
const res = await fetch(`/api/properties/${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bounds, filters }),
signal: controller.signal,
});
if (res.status === 429) { // back off, don't loop
const wait = Number(res.headers.get("Retry-After") || 5) * 1000;
timer = setTimeout(loadPins, wait);
return;
}
const body = await res.json();
cache.set(key, { at: Date.now(), body });
render(path, body); // pins: if body.total > 10,000, ask the user to zoom in
} catch (err) {
if (err.name !== "AbortError") throw err;
}
}// On your server — the only place your TitanSkip key lives.
app.post("/api/properties/:path(pins|count)", async (req, res) => {
const upstream = await fetch(
`https://api.titanskip.com/v1/properties/${req.params.path}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TITANSKIP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ bounds: req.body.bounds, filters: req.body.filters }),
}
);
const retryAfter = upstream.headers.get("Retry-After");
if (retryAfter) res.set("Retry-After", retryAfter);
res.status(upstream.status).json(await upstream.json());
});Get map pins
https://api.titanskip.com/v1/properties/pinsFree600 / min · 1,200 / accountEvery property matching an area and filters, as map pins — just an id and coordinates. Free: no credits are charged. Results are paged up to 10,000 pins at a time; `total` tells you how many match and `next_offset` gives the next page (null on the last one). A pin's `id` is what POST /properties/lookup and POST /property-exports take. Pins count toward your daily pin allowance (see Map integration guide).
Field requirements
areaSend exactly one way of describing the area: polygon, bounds, zip, city, county, state, or latitude + longitude. A request with no area is rejected — an unscoped search is the whole country.
Filters are optional and go inside a single `filters` object. Every filter listed below is supported; anything else returns 400 with the unknown name.
| Parameter | Type | Required | Description |
|---|---|---|---|
polygon | array | one of · area | Area to search as [[lat, lng], …] — 3 to 500 points, e.g. the map viewport or a drawn shape. |
bounds | object | one of · area | Alternative to polygon: { north, south, east, west } in degrees. |
zip | string | one of · area | Alternative area: a ZIP code. |
city | string | one of · area | Alternative area: city name (pair with state to disambiguate). |
county | string | one of · area | Alternative area: county name. |
state | string | one of · area | Alternative area: 2-letter state code. Can be combined with city or county. |
latitude / longitude | number | one of · area | Alternative area: a point, sent together. Add radius to widen it. |
radius | number | optional | Miles around latitude / longitude. |
filters · property | object | optional | Property Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · owner | object | optional | Owner Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · financial | object | optional | Financial Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · foreclosure | object | optional | Foreclosure Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · mls | object | optional | MLS Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
limit | integer | optional | Pins per page, 1–10,000. Default: 10,000. |
offset | integer | optional | Pins to skip — pass the previous response's next_offset. Default: 0. |
POST /properties/pins
Content-Type: application/json
{
"bounds": { "north": 32.82, "south": 32.78, "east": -96.76, "west": -96.82 },
"filters": {
"absentee_owner": true,
"property_type": "SFR",
"estimated_equity_min": 100000
},
"limit": 10000
}{
"data": [
{ "id": "166808412", "latitude": 32.7612, "longitude": -96.8231 },
{ "id": "166808977", "latitude": 32.7619, "longitude": -96.8204 }
],
"total": 1843,
"count": 1843,
"offset": 0,
"limit": 10000,
"next_offset": null
}Count properties
https://api.titanskip.com/v1/properties/countFree600 / min · 1,200 / accountHow many properties match an area and filters, without the pins. Free. Use it when the map is zoomed out — a state-wide view can match millions of properties, which no map should try to draw. Show the number and ask the user to zoom in.
Field requirements
areaSend exactly one way of describing the area: polygon, bounds, zip, city, county, state, or latitude + longitude. A request with no area is rejected — an unscoped search is the whole country.
Filters are optional and go inside a single `filters` object. Every filter listed below is supported; anything else returns 400 with the unknown name.
| Parameter | Type | Required | Description |
|---|---|---|---|
polygon | array | one of · area | Area to search as [[lat, lng], …] — 3 to 500 points, e.g. the map viewport or a drawn shape. |
bounds | object | one of · area | Alternative to polygon: { north, south, east, west } in degrees. |
zip | string | one of · area | Alternative area: a ZIP code. |
city | string | one of · area | Alternative area: city name (pair with state to disambiguate). |
county | string | one of · area | Alternative area: county name. |
state | string | one of · area | Alternative area: 2-letter state code. Can be combined with city or county. |
latitude / longitude | number | one of · area | Alternative area: a point, sent together. Add radius to widen it. |
radius | number | optional | Miles around latitude / longitude. |
filters · property | object | optional | Property Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · owner | object | optional | Owner Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · financial | object | optional | Financial Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · foreclosure | object | optional | Foreclosure Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
filters · mls | object | optional | MLS Filters. All filters are AND-ed. Unknown filter names are rejected with a 400. |
POST /properties/count
Content-Type: application/json
{
"state": "TX",
"filters": { "pre_foreclosure": true }
}{
"total": 48211
}Search suggestions
https://api.titanskip.com/v1/properties/autocompleteFree600 / min · 1,200 / accountSuggestions for a search box as the user types an address, city, neighborhood, county, state or ZIP. Free. Address suggestions carry a `property_id`, so picking one can go straight to POST /properties/lookup. Debounce keystrokes (~250 ms) and only call once 3+ characters are typed.
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | required | What the user typed. Fewer than 3 characters returns an empty list without searching. |
types | string[] | optional | Limit suggestion types: A address, C city, G neighborhood, N county, T state, Z zip. Default: all. |
POST /properties/autocomplete
Content-Type: application/json
{ "search": "1420 N Beck" }{
"search": "1420 N Beck",
"suggestions": [
{
"title": "1420 N Beckley Ave, Dallas, TX 75203",
"type": "A",
"city": "Dallas",
"state": "TX",
"county": "Dallas",
"zip": "75203",
"latitude": 32.7612,
"longitude": -96.8231,
"property_id": "166808412"
}
]
}Look up a property record
https://api.titanskip.com/v1/properties/lookupUses credits120 / min · 240 / accountBuy the full record for one property — owner, mailing address, valuation, equity, mortgage, sale history, distress and MLS flags (~150 fields). Charges rates.property credits (GET /pricing) and returns the record inline. A property your account has already bought is returned free (already_owned: true), so lookups are safe to retry. If no record exists you're not charged. If it's still being fetched you get 202 — poll GET /properties/:id, which never charges.
| Parameter | Type | Required | Description |
|---|---|---|---|
property_id | string | required | Numeric property ID from a pin (data[].id) or an address suggestion (property_id). |
POST /properties/lookup
Content-Type: application/json
{ "property_id": "166808412" }{
"property_id": "166808412",
"status": "completed",
"record": {
"Property Id": "166808412",
"Apn": "00000819441000000",
"Address Address": "1420 N Beckley Ave, Dallas, TX 75203",
"Address City": "Dallas",
"Address State": "TX",
"Address Zip": "75203",
"Latitude": "32.7612",
"Longitude": "-96.8231",
"Property Type": "SFR",
"Bedrooms": "3",
"Bathrooms": "2",
"Square Feet": "1640",
"Year Built": "1948",
"Estimated Value": "412000",
"Estimated Equity": "268000",
"Owner 1 First Name": "Maria",
"Owner 1 Last Name": "Delgado",
"Absentee Owner": "yes",
"Vacant": "no",
"Last Sale Date": "2009-04-17",
"Last Sale Amount": "121000"
},
"summary": {
"address": "1420 N Beckley Ave, Dallas, TX 75203",
"city": "Dallas",
"state": "TX",
"zip": "75203"
},
"credits_charged_now": 2,
"already_owned": false,
"purchase": {
"rate": 2,
"credits_charged": 2,
"credits_refunded": 0,
"purchased_at": "2026-10-17T15:02:11.000Z"
}
}Get a purchased property
https://api.titanskip.com/v1/properties/:idFree1,200 / min · 2,400 / accountRead a property record your account already bought. Never charges and never starts a lookup — POST /properties/lookup is the only call that spends credits, so polling this after a 202 is always free. Same response shape as the lookup.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Property ID (in the URL path). |
GET /properties/166808412{
"property_id": "166808412",
"status": "completed",
"record": { "Property Id": "166808412", "Address City": "Dallas", "…": "…" },
"summary": { "address": "1420 N Beckley Ave, Dallas, TX 75203", "city": "Dallas", "state": "TX", "zip": "75203" },
"credits_charged_now": 0,
"already_owned": true,
"purchase": { "rate": 2, "credits_charged": 2, "credits_refunded": 0, "purchased_at": "2026-10-17T15:02:11.000Z" }
}List purchased properties
https://api.titanskip.com/v1/propertiesFree1,200 / min · 2,400 / accountProperties your account has bought one at a time (through POST /properties/lookup or the dashboard), newest first. These are receipts; fetch a record with GET /properties/:id.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | optional | Page number. Default: 1. |
limit | integer | optional | Per page. Default: 20. Maximum: 100. |
GET /properties?page=1&limit=20{
"_metadata": { "page": 1, "per_page": 20, "page_count": 1, "total_count": 2 },
"properties": [
{
"property_id": "166808412",
"name": "1420 N Beckley Ave, Dallas, TX 75203",
"status": "completed",
"rate": 2,
"credits_charged": 2,
"credits_refunded": 0,
"purchased_at": "2026-10-17T15:02:11.000Z",
"completed_at": "2026-10-17T15:02:12.000Z"
}
]
}Create a property export
https://api.titanskip.com/v1/property-exportsUses credits10 / min · 20 / accountBuy full records for up to 10,000 properties at once — typically the pins in an area. Asynchronous, like POST /traces: you get the export back immediately and a property_export.completed webhook when it's done (or poll GET /property-exports/:id). Charges unique IDs × rates.property up front; IDs with no record are refunded on completion. At most 3 exports can be processing per account at once.
| Parameter | Type | Required | Description |
|---|---|---|---|
ids | string[] | required | Property IDs from /properties/pins. Duplicates are removed before charging. Maximum 10,000. |
name | string | optional | A label for the export. Default: "N properties". |
POST /property-exports
Content-Type: application/json
{
"name": "Dallas absentee owners",
"ids": ["166808412", "166808977", "166809103"]
}{
"export": {
"id": "0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10",
"encoded_id": "pex_AZLxxHoufBGeC10fijwrEA",
"name": "Dallas absentee owners",
"status": "processing",
"total_rows": 3,
"match_count": 0,
"no_record_count": 0,
"error_count": 0,
"rate": 2,
"credits_charged": 6,
"credits_refunded": 0,
"download_url": null,
"created_at": "2026-10-17T15:02:11.000Z",
"completed_at": null,
"error_message": null
}
}List property exports
https://api.titanskip.com/v1/property-exportsFree1,200 / min · 2,400 / accountYour property exports, newest first.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | optional | Page number. Default: 1. |
limit | integer | optional | Per page. Default: 10. Maximum: 100. |
GET /property-exports?page=1&limit=10{
"_metadata": { "page": 1, "per_page": 10, "page_count": 1, "total_count": 1 },
"exports": [
{
"id": "0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10",
"encoded_id": "pex_AZLxxHoufBGeC10fijwrEA",
"name": "Dallas absentee owners",
"status": "completed",
"total_rows": 2500,
"match_count": 2461,
"no_record_count": 39,
"error_count": 0,
"rate": 2,
"credits_charged": 5000,
"credits_refunded": 78,
"download_url": "https://api.titanskip.com/v1/property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10/download",
"created_at": "2026-10-17T15:02:11.000Z",
"completed_at": "2026-10-17T15:04:40.000Z",
"error_message": null
}
]
}Get a property export
https://api.titanskip.com/v1/property-exports/:idFree1,200 / min · 2,400 / accountOne export's status and receipt. The shape matches each item in GET /property-exports and the property_export.* webhook's data field, so one parser works for polling and webhooks.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Export ID or its pex_ encoded_id (in the URL path). |
GET /property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10{
"export": {
"id": "0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10",
"encoded_id": "pex_AZLxxHoufBGeC10fijwrEA",
"name": "Dallas absentee owners",
"status": "completed",
"total_rows": 2500,
"match_count": 2461,
"no_record_count": 39,
"error_count": 0,
"rate": 2,
"credits_charged": 5000,
"credits_refunded": 78,
"download_url": "https://api.titanskip.com/v1/property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10/download",
"created_at": "2026-10-17T15:02:11.000Z",
"completed_at": "2026-10-17T15:04:40.000Z",
"error_message": null
}
}Get export records (JSON)
https://api.titanskip.com/v1/property-exports/:id/rowsFree120 / min · 240 / accountA completed export's records as paged JSON — the same columns, in the same order, as the CSV download. Free; the records were paid for at purchase. Returns 409 until the export has completed.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Export ID (in the URL path). |
page | integer | optional | Page number. Default: 1. |
limit | integer | optional | Rows per page. Default: 50. Maximum: 100. |
GET /property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10/rows?page=1&limit=50{
"_metadata": { "page": 1, "per_page": 50, "page_count": 50, "total_count": 2500 },
"columns": ["Property Id", "Id", "Apn", "Parcel Account Number", "Address Street", "…"],
"rows": [
{ "row_index": 0, "status": "completed", "record": { "Property Id": "166808412", "Address City": "Dallas", "…": "…" } },
{ "row_index": 1, "status": "no_record", "record": null }
]
}Download an export (CSV)
https://api.titanskip.com/v1/property-exports/:id/downloadFree1,200 / min · 2,400 / accountStreams a completed export as CSV — this is the export's download_url. Re-downloading is free. Returns 409 until the export has completed.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Export ID (in the URL path). |
GET /property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10/downloadHTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="Properties-Dallas_absentee_owners.csv"
Property Id,Id,Apn,Parcel Account Number,Address Street,Address City,…
166808412,166808412,00000819441000000,,1420 N Beckley Ave,Dallas,…Retry the trace completion webhook
https://api.titanskip.com/v1/traces/:id/retry-webhook120 / minuteRequeues a fresh delivery to the webhook URL configured on the API key that created this trace. Useful when your endpoint was down during the original delivery. The trace state is not recomputed and credits are not re-evaluated — only the outbound notification is re-fired.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Trace ID (in the URL path). Must be a completed or failed trace owned by your API key. |
POST /traces/trc_19568a12b3c7d4e8f5a6b7c8d9e/retry-webhook{
"delivery_id": "whd_19568a12b3c7d4e8f5a6b7c8d9e",
"attempt": 1,
"status": "pending"
}Retry the DNC completion webhook
https://api.titanskip.com/v1/dnc/:id/retry-webhook120 / minuteRequeues a fresh delivery to the webhook URL configured on the API key that created this DNC check. Useful when your endpoint was down during the original delivery. The DNC job state is not recomputed and credits are not re-evaluated — only the outbound notification is re-fired.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | DNC job ID (in the URL path). Must be a completed or failed job owned by your API key. |
POST /dnc/dnc_19568a12b3c7d4e8f5a6b7c8d9e/retry-webhook{
"delivery_id": "whd_19568a12b3c7d4e8f5a6b7c8d9e",
"attempt": 1,
"status": "pending"
}Retry the property export webhook
https://api.titanskip.com/v1/property-exports/:id/retry-webhook120 / min · 240 / accountRequeues a fresh property_export.* delivery to the webhook URL configured on the API key making this call. The export itself is not recomputed — only the notification is re-sent.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Export ID (in the URL path). Must be completed or failed. |
POST /property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10/retry-webhook{
"delivery_id": "whd_19568a12b3c7d4e8f5a6b7c8d9e",
"attempt": 1,
"status": "pending"
}Completion webhook
https://api.titanskip.com/v1(your webhook URL)We POST to your configured webhook URL when an asynchronous job finalises (a bulk trace, DNC check or property export). Every event uses the same typed envelope — { event, data } — so you can route in a single switch. The `data` field mirrors the exact shape that GET /v1/<type>/:id returns. Single-record traces and property lookups are synchronous and do not fire webhooks.
| Parameter | Type | Required | Description |
|---|---|---|---|
X-TitanSkip-Event | header | required | Event name: trace.completed, trace.failed, dnc.completed, dnc.failed, property_export.completed, or property_export.failed. Mirrors `event` in the body. |
X-TitanSkip-Signature | header | required | HMAC-SHA256 of the raw request body, hex-encoded. Signed with the webhook secret from your API Keys page. |
X-TitanSkip-Delivery-Id | header | required | Unique ID for this delivery attempt. Dedupe on this for at-most-once handling. |
X-TitanSkip-Attempt | header | required | Attempt number (1–5). Retries share the resource ID inside `data` as the stable dedupe key. |
// trace.completed (bulk skip trace)
// On trace.failed the same shape is sent with event="trace.failed",
// data.status="failed", download_url=null, and error_message populated.
{
"event": "trace.completed",
"data": {
"id": "trc_19568a12b3c7d4e8f5a6b7c8d9e",
"status": "completed",
"mode": "normal",
"include_llcs": true,
"total_rows": 23000,
"entity_rows": 400,
"match_count": 22100,
"no_match_count": 750,
"error_count": 150,
"credits_charged": 23800,
"credits_refunded": 1100,
"download_url": "https://api.titanskip.com/v1/traces/trc_19568a12b3c7d4e8f5a6b7c8d9e/download",
"created_at": "2026-03-20T14:30:00.000Z",
"completed_at": "2026-03-20T15:00:00.000Z",
"error_message": null,
"breakdown": {
"individuals": { "rate": 1, "rows": 22600, "match": 21800, "no_match": 700, "error": 100, "credits_charged": 22600, "credits_refunded": 800 },
"llcs": { "rate": 3, "rows": 400, "match": 300, "no_match": 50, "error": 50, "credits_charged": 1200, "credits_refunded": 300 }
}
}
}
// dnc.completed (DNC + TCPA lookup)
// On dnc.failed the same shape is sent with event="dnc.failed",
// data.status="failed", download_url=null, and error_message populated.
{
"event": "dnc.completed",
"data": {
"id": "dnc_19568a12b3c7d4e8f5a6b7c8d9e",
"status": "completed",
"total_rows": 5000,
"match_count": 4980,
"error_count": 20,
"rate": 1,
"credits_charged": 5000,
"credits_refunded": 20,
"download_url": "https://api.titanskip.com/v1/dnc/dnc_19568a12b3c7d4e8f5a6b7c8d9e/download",
"created_at": "2026-03-20T14:30:00.000Z",
"completed_at": "2026-03-20T15:00:00.000Z",
"error_message": null
}
}
// property_export.completed (bulk property records)
// No-record ids are refunded: credits_refunded = no_record_count × rate.
{
"event": "property_export.completed",
"data": {
"id": "0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10",
"encoded_id": "pex_AZLxxHoufBGeC10fijwrEA",
"name": "Dallas absentee owners",
"status": "completed",
"total_rows": 2500,
"match_count": 2461,
"no_record_count": 39,
"error_count": 0,
"rate": 2,
"credits_charged": 5000,
"credits_refunded": 78,
"download_url": "https://api.titanskip.com/v1/property-exports/0192f1c4-7a2e-7c11-9e0b-5d1f8a3c2b10/download",
"created_at": "2026-10-17T15:02:11.000Z",
"completed_at": "2026-10-17T15:04:40.000Z",
"error_message": null
}
}// Reply with any 2xx within 10 seconds.
// We make up to 5 attempts in total, retrying after 5s, 30s, 5min and 30min.
HTTP/1.1 200 OK
Content-Type: application/json
{ "received": true }Error codes
Errors return a human-readable error string and, where it helps you branch, a machine-readable code. Trace mapping errors add a details array.
| Code | Name | Description |
|---|---|---|
| 400 | Bad Request | Missing or malformed request parameters. Check the error.details field for field-level messages. |
| 401 | Unauthorized | Missing or invalid API key. Send it as `Authorization: Bearer tsk_live_...`. |
| 402 | Payment Required | Insufficient credits (INSUFFICIENT_CREDITS), or a metered account that is past due or over its limit. |
| 403 | Forbidden | Account suspended, or a feature not enabled on your account (e.g. PROPERTY_DATA_DISABLED). |
| 404 | Not Found | The resource doesn't exist or isn't yours — or, for properties, NO_RECORD / NOT_PURCHASED. |
| 409 | Conflict | Not ready yet (e.g. downloading an unfinished export) or a webhook delivery already in flight. |
| 422 | Unprocessable | The request was well-formed but contained semantic errors, such as exceeding the record limit. |
| 429 | Rate Limited | Per-minute limit (key or account), DAILY_PIN_LIMIT, or TOO_MANY_IN_FLIGHT. Wait for the Retry-After header. |
| 500 | Server Error | An unexpected error occurred on our end. These are logged and automatically investigated. |
// 402 — not enough credits
{
"error": "Insufficient credits — needs 5000, you have 1200.",
"code": "INSUFFICIENT_CREDITS",
"required": 5000,
"available": 1200
}
// 429 — rate limited (wait Retry-After seconds)
{
"error": "Rate limit exceeded for this account (shared across all of its API keys)",
"limit": 1200,
"scope": "account",
"retry_after_seconds": 18
}