# TitanSkip API > TitanSkip is a skip-tracing and DNC/TCPA compliance API. Upload records (CSV or > a single lookup) and get back owner contact info (phones, emails) and Do-Not-Call > scrub results. Billing is credit-based. This file helps developers and LLM coding > assistants integrate the API correctly. - Base URL: `https://api.titanskip.com/v1` - Auth: bearer API key on every request — `Authorization: Bearer tsk_live_xxx` (create keys in the dashboard under API → Keys). - Request bodies: JSON (`application/json`) or `multipart/form-data` for file uploads. - All responses are JSON unless noted (CSV download endpoints stream a file). - Full HTML docs: https://app.titanskip.com/api-docs ## Core concepts - **Credits**: every job costs credits. Rates are per-row and vary by job type (`GET /pricing`). Credits are charged up front per billable row and **refunded** for no-matches and upstream errors after a job completes. - **Billing modes**: `prepaid` (a credit balance you top up) or `metered` (we charge the card on file once outstanding usage crosses a threshold). Check via `GET /account`. - **Async vs sync**: bulk uploads (`POST /traces`, `POST /dnc`) are **asynchronous** — you get a job id immediately, then receive a webhook (or poll) when it finishes. `POST /traces/single` is **synchronous** — the result comes back inline. - **Resource IDs**: each resource has an `id`. Responses also include an `encoded_id` (a prefixed form like `trc_…` / `dnc_…`). Either form is accepted wherever an id appears in a path (e.g. `GET /traces/:id`). Use the value exactly as returned. ## Rate limits Per API key, per **minute** (fixed window). Read endpoints are generous; writes are tighter. On exceed: `429` with a `Retry-After` header (seconds) and `X-RateLimit-*`. - `GET /traces`, `GET /traces/:id`, `…/download`, and the DNC equivalents: **1200/min** - `POST /traces`, `POST /traces/single`, `…/retry-webhook`, `POST /dnc`, `GET /account`, `GET /pricing`: **120/min** ## Endpoints ### GET /account Account snapshot: `id`, `email`, `billing_mode` (`prepaid`|`metered`), `credits`, `subscription` (or null), `metered` (or null). Use it to pre-flight before submitting. ### GET /pricing Per-row credit rates: `{ "rates": { "normal": 1, "enhanced": 2, "llc": 3, "dnc": 1 } }`. Fetch before estimating cost — rates can change. ### GET /traces?page=&limit=&type= Paginated trace jobs. `limit` ≤ 100. `type` = `bulk` | `single`. Returns `_metadata` (page/per_page/page_count/total_count) + `traces[]` (id, status, mode, total_rows, match_count, no_match_count, error_count, credits_charged, credits_refunded, download_url, created_at, completed_at). ### POST /traces (multipart/form-data) — bulk skip trace Fields: - `file` (required): CSV, ≤ 50 MB. - `mode` (optional): `normal` (default — name + address lookup; requires `first_name` + `last_name`) or `enhanced` (address-only deeper lookup; names ignored). - `column_mapping` (required): JSON string mapping our keys to your CSV headers. Rules: normal mode requires `first_name` + `last_name`; every job requires at least one FULL address group — property (`property_street|city|state|zip`) or mailing (`mailing_street|city|state|zip`). Address groups are all-or-nothing: map all four fields of a group or none. A mapped column may hold blank values on some rows (e.g. `last_name` on company rows). - `include_llcs` (optional, default false): also trace company/LLC rows (billed at the `llc` rate). Returns `201` with `{ success, id, status:"processing", mode, include_llcs, total_rows, entity_rows, cost, credits_charged, columns }`. Completion arrives via webhook. ```bash curl -X POST https://api.titanskip.com/v1/traces \ -H "Authorization: Bearer tsk_live_xxx" \ -F "file=@owners.csv" \ -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"}' ``` ### POST /traces/single (application/json) — synchronous single lookup Body: `first_name` (required; or a full company name), `last_name` (optional), `address`, `city`, `state`, `zip` (required), optional `mailing_address|city|state|zip`, optional `is_entity` (force LLC pricing). Charges 1 credit (full refund on no-match). Returns inline: `{ id, matched, first_name, last_name, phones:[{number,type}], emails:[...], credits, credits_refunded, is_entity }`. No webhook. ### GET /traces/:id One trace job — same shape as a `traces[]` item and the webhook `data` field (one parser works for polling and webhooks). Includes the `breakdown` object on `include_llcs` traces. ### GET /traces/:id/download Streams the result CSV (only once `status == "completed"`). This is the `download_url` returned on the trace object and in the completion webhook. ### POST /traces/:id/retry-webhook Re-fires the completion webhook for a completed/failed trace (state not recomputed). Returns `{ delivery_id, attempt, status }`. ### GET /dnc?page=&limit= Paginated DNC/TCPA jobs. Returns `_metadata` + `jobs[]` (id, name, status, total_rows, match_count, error_count, credits_charged, credits_refunded, download_url, created_at, completed_at). ### POST /dnc (multipart/form-data) — DNC + TCPA scrub Fields: `file` (CSV of phone numbers, ≤ 50 MB), `column_mapping` (JSON string mapping `phone` to your column, e.g. `{"phone":"Phone Number"}`; any number format is normalized). Returns `{ id, name, status, total_rows, rate, credits_charged }`. Completion via webhook. ### GET /dnc/:id · GET /dnc/:id/download · POST /dnc/:id/retry-webhook Same patterns as the trace equivalents. ## Webhooks (async completion) Set a webhook URL on the API key. We `POST` to it when a bulk trace or DNC job finalizes. Single-record traces are synchronous and never fire webhooks. Envelope (same for every event — route on `event`): ```json { "event": "trace.completed", "data": { "id": "trc_…", "status": "completed", "total_rows": 1000, "match_count": 870, "error_count": 20, "credits_charged": 1120, "credits_refunded": 130, "download_url": "https://api.titanskip.com/v1/traces/trc_…/download", "completed_at": "2026-03-17T16:03:42.000Z", "error_message": null } } ``` Events: `trace.completed`, `trace.failed`, `dnc.completed`, `dnc.failed`. On `*.failed`, `data.status="failed"`, `download_url=null`, `error_message` populated. **LLC breakdown (only on `include_llcs` traces).** When a trace included LLC/company rows, `data` also carries a `breakdown` so you can reconcile/refund per tier — individuals are billed at the normal/enhanced rate, LLCs at the LLC rate: ```json "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 } } ``` The two tiers sum to the top-level `match_count`/`no_match_count`/`error_count`/ `credits_refunded`. Absent on non-LLC traces. (Cache-hit counts are TitanSkip-internal and never exposed.) Headers on each delivery: - `X-TitanSkip-Event` — the event name (mirrors `event`). - `X-TitanSkip-Signature` — **HMAC-SHA256 of the raw request body, hex-encoded**, signed with your webhook secret (API Keys page). Verify it before trusting the payload. - `X-TitanSkip-Delivery-Id` — unique per attempt; dedupe on it (or on `data.id`). - `X-TitanSkip-Attempt` — 1–5. We auto-retry failed deliveries up to 5 times. Verification (Node): ```js import crypto from "node:crypto"; function verify(rawBody, signatureHeader, secret) { const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader)); } ``` Use the **raw** request body (not re-serialized JSON). Respond `2xx` quickly; do heavy work async. Treat deliveries as at-least-once and dedupe. ## Errors JSON shape: `{ "error": "human-readable message", "code": "OPTIONAL_CODE" }`. - `400` — malformed request (bad JSON, bad `column_mapping`). - `401` — missing/invalid API key. - `402` — insufficient credits, or metered account past due. - `403` — not allowed (e.g. banned). - `404` — resource not found / not owned by your key. - `413` — file exceeds 50 MB. - `422` — well-formed but semantically invalid (e.g. exceeds the per-job record limit). - `429` — rate limited; honor `Retry-After`. - `5xx` — transient; retry with backoff. ## Typical integration flow 1. `GET /pricing` and `GET /account` — confirm rates + that you have enough credits. 2. `POST /traces` (or `/dnc`) with your CSV + `column_mapping`. Store the returned `id`. 3. Either: receive the `trace.completed` / `dnc.completed` **webhook** (verify the signature), or **poll** `GET /traces/:id` until `status == "completed"`. 4. `GET /traces/:id/download` (the `download_url`) to fetch the result CSV. 5. Reconcile `credits_charged` − `credits_refunded` for actual spend. ## Notes for AI assistants writing integration code - Always send the `Authorization: Bearer` header; never hardcode keys — read from env. - For uploads use `multipart/form-data`; `column_mapping` is a JSON **string** field. - Prefer webhooks over polling; if polling, back off (e.g. 3–5s) and respect rate limits. - Verify webhook signatures against the **raw** body with HMAC-SHA256 (hex). - Don't assume an id is prefixed — pass back the `id`/`encoded_id` exactly as received.