Phantom Data · REST API v1

API Reference

Pull skip-traced homeowner records and TCPA/DNC-scrubbed lists straight from your code. Authenticated, prepaid, audit-logged.

Base URL https://vault.phantomdata.io Prefix /api/v1 Auth Bearer token Support blake@phantomdata.com

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

Required

Every billable endpoint requires a positive balance. Top up via the Phantom dashboard before you start.

Add funds →

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

CodeMeaning
401Missing or invalid Authorization header
401Key is unknown or has been revoked
403Account suspended — contact support
429Daily 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.

ServiceCharge
Order — homeowner record with mobile phoneprice_per_record × records_delivered
TCPA / DNC scrubtcpa_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

GET/api/v1/me

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
}
POST/api/v1/preview

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.

POST/api/v1/orders

Place an order against a set of ZIPs. Records are filtered to those with a mobile phone — we don’t deliver phoneless rows.

Body

FieldTypeRequiredNotes
zip_codesstring[] or csvyes5-digit US ZIPs
quantityintone ofTarget record count
want_allboolone ofTake everything available
filtersobjectnoSee filters below

Filters object

KeyTypeExample
property_typestring"single_family", "condo", "multi_family"
owner_occupiedbooltrue (skip absentee), false (only absentee)
min_valueint300000
max_valueint750000
tcpa_scrubbooltrue to drop TCPA litigators (adds per-record fee)
dnc_checkbooltrue 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

No order is created on 402

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"
}
GET/api/v1/orders/{id}

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.

GET/api/v1/orders

Last 50 orders, newest first.

curl -H "Authorization: Bearer $PV_KEY" \
     https://vault.phantomdata.io/api/v1/orders
GET/api/v1/orders/{id}/download

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.

POST/api/v1/scrub/upload

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, or cell
  • 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
}
GET/api/v1/scrub/{job_id}
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
}
GET/api/v1/scrub/{job_id}/download?type=clean|full
  • type=clean (default) — only rows where the phone is not TCPA/DNC flagged
  • type=full — every input row with two new columns: tcpa_litigator (bool) and dnc (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

CodeWhenWhat to do
400Missing or malformed request bodyCheck the error message; fix the request
401Bad/missing API keyVerify the Authorization header
402Insufficient balanceAdd funds at vault.phantomdata.io/billing, retry
403Account suspendedEmail blake@phantomdata.com
404Order/job not found, or not yoursCheck the ID
429Daily rate limit hitWait until UTC midnight, or request a higher limit
5xxServer-side issueRetry 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

  1. Top up before you batch. A failed order at the front of a queue is annoying — check /me for balance before scheduling large runs.
  2. Use POST /preview before big orders so you have a realistic ceiling on how many records exist in your ZIPs.
  3. Poll politely. 20–30 seconds between status checks is plenty.
  4. Save the order_id. It’s the only handle to download the CSV.
  5. 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.
  6. Never put your API key in client-side code or git. Rotate immediately if exposed.