API Reference
Pull skip-traced homeowner records and TCPA/DNC-scrubbed lists straight from your code. Authenticated, prepaid, audit-logged.
01Getting started
Get your API key
Your Phantom Data account manager issues you a key. It looks like this:
pv_a1b2c3d4_e5f67890abcdef1234567890abcdef1234567890abcdef12345678
Treat it like a password. Don’t commit it to source control. Don’t share it. If it leaks, contact support and we’ll rotate it.
Fund your account before calling paid endpoints
Every billable endpoint requires a positive balance. Top up via the Phantom dashboard before you start.
Log in with the email tied to your account, click Add Funds, and pay with your card. Funds are available immediately. Orders deduct from the balance as records deliver.
If your balance can’t cover an order, the API responds 402 Payment Required and no order is created — we don’t burn provider cost on accounts that can’t pay. The response includes the exact shortfall and a link to top up.
Quick test
# Verify your key works curl -H "Authorization: Bearer YOUR_API_KEY" \ https://vault.phantomdata.io/api/v1/me
If your key is valid, you’ll get back your account info, balance, and this month’s usage.
02Authentication
Every request must carry your key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Failure modes
| Code | Meaning |
|---|---|
| 401 | Missing or invalid Authorization header |
| 401 | Key is unknown or has been revoked |
| 403 | Account suspended — contact support |
| 429 | Daily request quota exceeded |
Each API key has a daily request limit (default 10,000). When you hit the cap, every request returns 429 until midnight UTC.
03Pricing & billing
Pricing is per-record and configured per-account at signup. Your effective rate is returned on GET /me as pricing.price_per_record.
| Service | Charge |
|---|---|
| Order — homeowner record with mobile phone | price_per_record × records_delivered |
| TCPA / DNC scrub | tcpa_price_per_record × phones_scrubbed |
| POST /preview (cache count) | Free |
| GET /me, GET /orders, GET /orders/{id} | Free |
Charges deduct from your balance the moment an order moves to completed. We don’t bill for phones we couldn’t match — orders that come back with fewer records than requested only charge for what was delivered.
If your balance falls below an in-flight want_all order’s cost, the worker trims output to what the balance covers. We never deliver more than you’ve paid for.
04Endpoints
Account info, current balance, and this month’s usage.
Request
curl -H "Authorization: Bearer $PV_KEY" \
https://vault.phantomdata.io/api/v1/me
Response 200
{
"client": { "id": 142, "name": "Acme Outbound", "email": "ops@acme.com" },
"pricing": { "type": "prepaid", "price_per_record": 0.05, "record_cap": 0 },
"balance": 850.00,
"trial_credits_remaining": 0,
"add_funds_url": "https://vault.phantomdata.io/billing",
"usage_this_month": { "records_pulled": 4321, "orders_placed": 8 },
"total_orders": 47
}
Cheap pre-flight to see roughly how much data is cached for a list of ZIPs. Reads cache only — does not deduct from your balance. Up to 100 ZIPs per call.
Request
curl -X POST https://vault.phantomdata.io/api/v1/preview \
-H "Authorization: Bearer $PV_KEY" \
-H "Content-Type: application/json" \
-d '{"zip_codes": ["33101", "33102", "33125"]}'
Response 200
{ "counts": { "33101": 12450, "33102": 8902, "33125": 14110 }, "total_cached": 35462 }
These are cached counts. Actual orders can pull beyond cache through our data providers.
Place an order against a set of ZIPs. Records are filtered to those with a mobile phone — we don’t deliver phoneless rows.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
zip_codes | string[] or csv | yes | 5-digit US ZIPs |
quantity | int | one of | Target record count |
want_all | bool | one of | Take everything available |
filters | object | no | See filters below |
Filters object
| Key | Type | Example |
|---|---|---|
property_type | string | "single_family", "condo", "multi_family" |
owner_occupied | bool | true (skip absentee), false (only absentee) |
min_value | int | 300000 |
max_value | int | 750000 |
tcpa_scrub | bool | true to drop TCPA litigators (adds per-record fee) |
dnc_check | bool | true to flag DNC numbers (adds per-record fee) |
Request
curl -X POST https://vault.phantomdata.io/api/v1/orders \
-H "Authorization: Bearer $PV_KEY" \
-H "Content-Type: application/json" \
-d '{
"zip_codes": ["33101", "33102"],
"quantity": 500,
"filters": { "property_type": "single_family", "owner_occupied": true }
}'
Response 201
{ "order_id": 8421, "status": "pending", "zips": 2 }
Response 402 — Insufficient balance
Add funds via the dashboard and retry. Nothing was charged.
{
"error": "Insufficient balance",
"message": "This order will cost ~$25.00 (500 records × $0.0500/record), but your balance is $4.10. Add funds at https://vault.phantomdata.io/billing before placing orders.",
"balance": 4.10,
"estimated_charge": 25.00,
"shortfall": 20.90,
"price_per_record": 0.05,
"add_funds_url": "https://vault.phantomdata.io/billing"
}
Order status.
Request
curl -H "Authorization: Bearer $PV_KEY" \
https://vault.phantomdata.io/api/v1/orders/8421
Response 200
{
"id": 8421,
"zip_codes": "33101,33102",
"quantity": 500,
"want_all": false,
"status": "completed",
"records_total": 487,
"records_with_phone": 487,
"has_download": true,
"started_at": "2026-05-29T14:02:11",
"completed_at": "2026-05-29T14:18:44",
"created_at": "2026-05-29T14:01:55"
}
Order lifecycle
pending → processing → scraping → enriching → completed
↓
failed
Poll every 10–30 seconds until status == "completed" (or "failed"). Typical processing is 5–25 minutes.
Last 50 orders, newest first.
curl -H "Authorization: Bearer $PV_KEY" \
https://vault.phantomdata.io/api/v1/orders
Returns the order’s CSV. Only works once status == "completed".
curl -H "Authorization: Bearer $PV_KEY" \
-o order_8421.csv \
https://vault.phantomdata.io/api/v1/orders/8421/download
CSV columns (typical)
First Name, Last Name, Best Mobile 1, Best Mobile 2, Address, City, State, ZIP, Email, Owner Occupied, Ownership Type, Landline, Mail Address, Mail City, Mail State, Mail ZIP, Mail ZIP+4, Co-Owner Name
When tcpa_scrub or dnc_check are enabled, extra DNC and TCPA Litigator columns are appended.
Upload a CSV with phone numbers and we’ll scrub each row against TCPA litigators and the DNC list. Returns a job_id you poll for status.
Required CSV columns (case-insensitive)
- A column whose name contains
phone,mobile, orcell - One of:
address,street,street_address,addr,address_line_1,address1 - One of:
zip,zipcode,zip_code,postal_code,postal
Request
curl -X POST https://vault.phantomdata.io/api/v1/scrub/upload \
-H "Authorization: Bearer $PV_KEY" \
-F "file=@my_list.csv" \
-F "list_name=April outbound batch"
Response 201
{ "job_id": 8500, "status": "pending", "rows": 12000, "valid_phones": 11842, "scrubbable_rows": 11800 }
Response 402 — Insufficient balance
{
"error": "Insufficient balance",
"message": "This scrub will cost ~$59.00 (11,800 phones × $0.0050/phone), but your balance is $4.10. Add funds via the dashboard or contact support.",
"estimated_charge": 59.00,
"balance": 4.10,
"shortfall": 54.90
}
curl -H "Authorization: Bearer $PV_KEY" \
https://vault.phantomdata.io/api/v1/scrub/8500
{
"job_id": 8500,
"status": "completed",
"phones_scrubbed": 11800,
"tcpa_litigators": 47,
"dnc_flagged": 2103,
"clean_count": 9650,
"has_download": true
}
type=clean(default) — only rows where the phone is not TCPA/DNC flaggedtype=full— every input row with two new columns:tcpa_litigator(bool) anddnc(bool)
curl -H "Authorization: Bearer $PV_KEY" \ -o scrubbed_clean.csv \ "https://vault.phantomdata.io/api/v1/scrub/8500/download?type=clean"
05End-to-end examples
Bash — place an order and download when ready
#!/usr/bin/env bash set -euo pipefail API="https://vault.phantomdata.io/api/v1" KEY="${PV_KEY:?set PV_KEY env var}" H="Authorization: Bearer $KEY" # 1. Check balance curl -s -H "$H" "$API/me" | jq '{balance, "price/rec": .pricing.price_per_record}' # 2. Place an order ORDER=$(curl -s -X POST "$API/orders" -H "$H" \ -H "Content-Type: application/json" \ -d '{"zip_codes":["33101","33102"],"quantity":500, "filters":{"property_type":"single_family","owner_occupied":true}}') ID=$(echo "$ORDER" | jq -r .order_id) echo "Order #$ID placed" # 3. Poll until done while :; do STATUS=$(curl -s -H "$H" "$API/orders/$ID" | jq -r .status) echo " status=$STATUS" [[ "$STATUS" == "completed" || "$STATUS" == "failed" ]] && break sleep 20 done # 4. Download curl -s -H "$H" -o "order_$ID.csv" "$API/orders/$ID/download" echo "Saved order_$ID.csv"
Python — same flow
import os, time, requests API = "https://vault.phantomdata.io/api/v1" H = {"Authorization": f"Bearer {os.environ['PV_KEY']}"} # Check balance me = requests.get(f"{API}/me", headers=H).json() print(f"Balance: ${me['balance']:.2f} Rate: ${me['pricing']['price_per_record']}/rec") # Place order order = requests.post( f"{API}/orders", headers={**H, "Content-Type": "application/json"}, json={ "zip_codes": ["33101", "33102"], "quantity": 500, "filters": {"property_type": "single_family", "owner_occupied": True}, }, ) if order.status_code == 402: print("Insufficient balance:", order.json()["message"]) raise SystemExit(1) oid = order.json()["order_id"] print(f"Order #{oid} placed") # Poll while True: s = requests.get(f"{API}/orders/{oid}", headers=H).json() print(f" status={s['status']}") if s["status"] in ("completed", "failed"): break time.sleep(20) # Download csv = requests.get(f"{API}/orders/{oid}/download", headers=H).content open(f"order_{oid}.csv", "wb").write(csv) print(f"Saved order_{oid}.csv ({len(csv):,} bytes)")
06Error reference
| Code | When | What to do |
|---|---|---|
| 400 | Missing or malformed request body | Check the error message; fix the request |
| 401 | Bad/missing API key | Verify the Authorization header |
| 402 | Insufficient balance | Add funds at vault.phantomdata.io/billing, retry |
| 403 | Account suspended | Email blake@phantomdata.com |
| 404 | Order/job not found, or not yours | Check the ID |
| 429 | Daily rate limit hit | Wait until UTC midnight, or request a higher limit |
| 5xx | Server-side issue | Retry with exponential backoff; if persistent, email support |
All error responses are JSON with at least {"error": "..."} and usually a longer "message" you can show your team.
07Best practices
- Top up before you batch. A failed order at the front of a queue is annoying — check
/mefor balance before scheduling large runs. - Use POST /preview before big orders so you have a realistic ceiling on how many records exist in your ZIPs.
- Poll politely. 20–30 seconds between status checks is plenty.
- Save the order_id. It’s the only handle to download the CSV.
- Match rates vary by ZIP. Mobile-phone match rates can be low in transient or military-heavy areas. If you see a small delivery count, it’s because we filtered out rows with no mobile — you only pay for what’s delivered.
- Never put your API key in client-side code or git. Rotate immediately if exposed.