DataSkip Developers / CLI · SDK · REST
Skip trace in
one command.
Turn a US property address into the owner's name, phone numbers with DNC flags, emails, and mailing address. One npm package gives you a CLI and a typed JS/TS SDK; the REST API works from any language. It is the same flat 4¢ per match as the dashboard rate sheet, and misses are always free. For pricing context and how to compare providers, start with the skip tracing API overview.
npm install -g skiptrace
Get an API Key
- 4¢per match
- $0per miss
- 1package, 3 ways in
For AI Agents
Vibe coder?
Copy a ready-made brief with the endpoint, auth, SDK, and error handling, then paste it into your AI coding agent and let it wire up DataSkip for you.
- Claude
- Codex
- Cursor
Everything your agent needs, in one paste.
Prefer a spec? openapi.json (OpenAPI 3.1) · developers.md (this page in markdown) · llms.txt (site summary with usage guidance)
01 / CLI
The CLI: skip trace from your terminal
The DataSkip CLI ships in the official npm package skiptrace (Node 18+, Apache-2.0). Create an API key in the dashboard (Settings → API), save it once, then trace addresses straight from your shell. Exit codes are script-friendly: 0 match, 1 miss, 2 usage error, 3 API error.
$ skiptrace config set-key pc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ skiptrace "123 Example St, Phoenix AZ 85001"
$ skiptrace lookup "44 Pine St" --city Bridgewater --state MA --zip 02324
$ skiptrace "123 Main St, Austin TX 78701" --json | jq '.phones'
02 / npm package
The SDK: skip trace from your code
The skiptrace package imports as a typed library. The API key falls back to the SKIPTRACE_API_KEY env var, then the saved CLI config, so new SkipTrace() with no arguments just works. It also ships skipBulk for up to 100 addresses in one call.
import { SkipTrace, ApiError } from 'skiptrace';
const st = new SkipTrace({ apiKey: process.env.SKIPTRACE_API_KEY });
try {
const result = await st.skip('123 Example St, Phoenix AZ 85001');
if (result.found) {
console.log(result.contact.fullName, result.phones, result.emails);
}
} catch (err) {
if (err instanceof ApiError && err.status === 402) {
// insufficient balance
}
}
import { SkipTrace, ApiError } from 'skiptrace';
const st = new SkipTrace({ apiKey: process.env.SKIPTRACE_API_KEY });
try {
const result = await st.skip('123 Example St, Phoenix AZ 85001');
if (result.found) {
console.log(result.contact.fullName, result.phones, result.emails);
}
} catch (err) {
if (err instanceof ApiError && err.status === 402) {
// insufficient balance
}
}
import { SkipTrace, ApiError } from 'skiptrace';
import type { LookupInput, LookupResponse } from 'skiptrace';
const st = new SkipTrace({ apiKey: process.env.SKIPTRACE_API_KEY });
const input: LookupInput = { address: '44 Pine St', city: 'Bridgewater', state: 'MA', zip: '02324' };
const result: LookupResponse = await st.skip(input);
if (result.found && result.contact) {
console.log(result.contact.fullName, result.phones, result.emails);
}
03 / REST API
The DataSkip REST API: skip trace from anywhere
One endpoint, any language, with your API key as a Bearer token. A hit charges 4¢ and returns the full contact; a miss returns found: false and charges nothing.
POST https://app.dataskip.io/api/v1/skip-trace
$ curl -X POST https://app.dataskip.io/api/v1/skip-trace \
-H "Authorization: Bearer pc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"address": "44 Pine St",
"city": "Bridgewater",
"state": "MA",
"zip": "02324"
}'
$ curl -X POST https://app.dataskip.io/api/v1/skip-trace \
-H "Authorization: Bearer pc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"address": "44 Pine St",
"city": "Bridgewater",
"state": "MA",
"zip": "02324"
}'
import os
import requests
resp = requests.post(
"https://app.dataskip.io/api/v1/skip-trace",
headers={"Authorization": f"Bearer {os.environ['SKIPTRACE_API_KEY']}"},
json={"address": "44 Pine St", "city": "Bridgewater", "state": "MA", "zip": "02324"},
)
data = resp.json()
if data["found"]:
print(data["contact"]["fullName"], data["phones"], data["emails"])
<?php
$ch = curl_init('https://app.dataskip.io/api/v1/skip-trace');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SKIPTRACE_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['address' => '44 Pine St', 'city' => 'Bridgewater', 'state' => 'MA', 'zip' => '02324']),
]);
$data = json_decode(curl_exec($ch), true);
if ($data['found']) {
echo $data['contact']['fullName'];
}
{
"success": true,
"found": true,
"charged": 4,
"contact": {
"firstName": "J•••",
"lastName": "C•••••",
"fullName": "J••• C•••••",
"propertyAddress": "44 Pine St",
"propertyCity": "Bridgewater",
"propertyState": "MA",
"propertyZip": "02324",
"mailingAddress": "44 Pine St",
"mailingCity": "Bridgewater",
"mailingState": "MA",
"mailingZip": "02324"
},
"phones": [
{ "number": "(•••) •••-0182", "type": "mobile", "dnc": false },
{ "number": "(•••) •••-4415", "type": "landline", "dnc": true }
],
"emails": ["j•••••@•••••.com"]
}
| Status | Meaning |
|---|---|
| 200 | Lookup ran. Check found: a miss is still 200 with contact: null and no charge. |
| 400 | Missing or invalid address in the JSON body. |
| 401 | Missing or invalid API key. |
| 402 | Insufficient balance to cover a match. Top up in the dashboard. |
| 429 | Rate limited. Retry after the indicated delay. |
04 / Bulk API
The bulk API: 100 per request, or a whole CSV
Skip trace up to 100 addresses in one API call, or a whole CSV in one job (never loop the single-lookup endpoint, which is limited to 500 lookups per minute per account). Either way you only pay for hits: misses are refunded automatically, even in bulk. The CSV pipeline handles bigger lists, up to 250,000 records, and previews the matched count and exact cost before anything is charged.
POST https://app.dataskip.io/api/v1/skip-trace-bulk
{
"addresses": [
{ "address": "123 Sample Ave", "city": "Anytown", "state": "CA", "zip": "90210" },
{ "address": "456 Demo Blvd", "zip": "90211" }
]
}
{
"success": true,
"total": 2,
"matched": 1,
"totalCharged": 4,
"results": [
{ "found": true, "charged": 4, "contact": { "fullName": "J••• C•••••" }, "phones": ["..."], "emails": ["..."] },
{ "found": false, "charged": 0, "contact": null, "phones": [], "emails": [] }
]
}
Up to 100 entries per request; only the address field is required per entry, and results come back in request order with the same shape as a single lookup. Billing matches single lookups: 4¢ per match, misses free. The worst case (every entry matching) is charged up front, a 402 with balanceRequired means the balance cannot cover the batch, and non-matches are refunded automatically when the request completes, so the net charge is always matches only. Rate limit: 250 bulk requests per minute per account, and 429 responses carry a Retry-After header.
await st.skipBulk(['123 Example St, Phoenix AZ 85001', { address: '44 Pine St', city: 'Bridgewater', state: 'MA', zip: '02324' }]);
In the SDK, skipBulk is on the client and as a one-shot export, fully typed (BulkLookupResponse). Free-form strings and structured objects mix in the same array.
For bigger lists, the CSV job pipeline takes one file of up to 250,000 addresses. Jobs created through the API also appear in the dashboard, and skiptrace bulk in the CLI runs this same pipeline.
$ curl -X POST https://app.dataskip.io/api/user/skip-trace \
-H "Authorization: Bearer pc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"filename": "leads.csv",
"columnMapping": { "address": "Property Address", "city": "City", "state": "State", "zip": "Zip" }
}'
- Create a job with
POST /api/user/skip-traceand your CSV's column mapping (the address, city, state, and ZIP columns must all be mapped; owner first and last name columns are optional). The response returns ajobIdand anuploadUrl. - Upload the raw CSV. Presigned uploads PUT the file to
uploadUrlwithContent-Type: text/csvand then call theuploadedendpoint; direct uploads POST the file as multipart form data. - Poll the job until its status is
preview. The preview reports total rows, matched rows, and the total charge in cents. - Confirm to run the charge, or cancel free of charge. Only matched rows are billed, at 4¢ each; a 402 response means the balance cannot cover the job.
- Poll until
completed, then fetch the enriched CSV fromdownloadUrl. Matched rows gain owner names, phones, and emails; unmatched rows come back unchanged.
| Endpoint | What it does |
|---|---|
| POST /api/user/skip-trace | Create a bulk job. Returns jobId and uploadUrl. |
| POST /api/user/skip-trace/{jobId}/uploaded | Notify after a presigned upload finishes. |
| GET /api/user/skip-trace/{jobId} | Job status. Also refreshes an expired downloadUrl on completed jobs. |
| POST /api/user/skip-trace/{jobId}/confirm | Confirm the previewed cost and charge the balance. |
| POST /api/user/skip-trace/{jobId}/cancel | Cancel before confirming. No charge. |
| GET /api/user/skip-trace | List the account's 50 most recent jobs. |
Job statuses: pending · uploaded · processing · preview · confirmed · generating · completed · failed · cancelled. A job never charges before the preview is confirmed.
05 / Errors
Errors: always JSON, always an error message
Every error response from the DataSkip API is JSON with an error string that names the problem, and on 400 the invalid field. A 402 adds balanceRequired (the cents needed to run the request); a CSV job confirm can add dailyMissLimit when the account is over its daily unmatched-list cap. A miss is not an error: it is HTTP 200 with found: false.
{ "error": "Insufficient balance", "balanceRequired": 4 }
| Status | Meaning | What to do |
|---|---|---|
| 400 | Body is not JSON, or a required field is missing or malformed; error names it. | Fix the field and retry. |
| 401 | Missing, malformed, invalid, or revoked API key, or the account is not active. | Send Authorization: Bearer <key>; create keys in the dashboard under Settings, API. |
| 402 | The balance cannot cover the request; balanceRequired is in cents. | Top up at app.dataskip.io, then retry. |
| 404 | CSV jobs only: no job with that jobId on this account. | Check the id returned when the job was created. |
| 409 | CSV jobs only: the job is not in a status that allows the action; error names the current status. | Poll the job and act on its current status. |
| 429 | Rate limited (Retry-After header, seconds). A CSV job confirm may also return the daily unmatched-list cap (dailyMissLimit in the body). | Wait for Retry-After; for the daily cap, clean the list and retry the next day. |
| 500 | Server error. A failed lookup is never charged. | Retry later; email support@dataskip.io if it persists. |
Always call the documented method: the lookup endpoints are POST only, and an unsupported method or an unknown path returns the app's own 405 or 404 page rather than an API error body, so check the path and method against openapi.json before calling.
06 / Rate limits
Rate limits: per account, per minute
| Endpoint | Limit |
|---|---|
| POST /api/v1/skip-trace | 500 lookups per minute per account |
| POST /api/v1/skip-trace-bulk | 250 requests per minute per account (100 addresses each) |
| Single and bulk, unmatched fair use | Misses are free up to 250 net unmatched addresses per UTC day, and four matches forgive one miss, so a list with a normal hit rate never reaches it. Past that, further misses are charged at the per-lookup rate instead of being blocked. |
| POST /api/contact | 5 messages per 15 minutes per IP |
A 429 carries a Retry-After header in seconds: wait that long, then retry. Never loop the single-lookup endpoint over a list; the bulk endpoint takes 100 addresses per call and the CSV pipeline takes 250,000 per job.
07 / Versioning
Versioning and deprecation policy
The DataSkip API is versioned in the URL path: /api/v1 is the current stable version, and openapi.json describes it. Backward-compatible changes (new endpoints, new optional request fields, new response fields) ship in place without a version change, so clients should ignore fields they do not recognize. Breaking changes ship as a new path version and never change how v1 behaves.
When a version or endpoint is scheduled for retirement, DataSkip announces it at least 6 months before the retirement date. During that notice period every response from the affected version carries a Deprecation header (RFC 9745) and a Sunset header (RFC 8594) with the retirement date, plus a Link with rel="deprecation" pointing at the announcement. The same dates are published on this page, in developers.md, and in llms.txt. Nothing is deprecated today.
Ship it in an
afternoon.
Get Your API Key
4¢ per match · misses free · support@dataskip.io