APIUpdated 2026-07-17

Automating Skip Tracing with the Acquired Data API

An end-to-end automation pattern: submit lists, receive signed webhooks, verify signatures, and load results into your CRM.

If you trace a list once a month, the dashboard is all you need. But once your team is running lists weekly — or you're feeding leads from multiple markets into one CRM — the manual loop starts to hurt: export a CSV, upload it, wait, remember to check back, download results, re-import, fix column mappings, repeat. Every hop is a place where a list sits idle for a day or a mapping breaks. The Acquired Data API is live today, and it collapses that loop into a pipeline that runs itself.

The architecture

The whole integration is three moves:

  1. Submit: POST /api/v1/jobs with your rows as JSON or a raw CSV (up to 10,000 rows per job). Propwire and PropStream exports work unmodified — headers are matched case- and punctuation-insensitively and unknown columns are ignored. Pass a callback_url so you don't have to poll.
  2. Receive: when the job resolves, Acquired Data POSTs a flat JSON payload (job.completed or job.failed) to your URL, signed with an X-AcquiredData-Signature header. Verify the HMAC before trusting it.
  3. Ingest: pull GET /api/v1/jobs/{id}/results page by page and upsert the flat row objects into your CRM. There's also a signed download_url if you'd rather grab the CSV.

Here's the pattern end to end in Node:

skip-trace-pipeline.mjs
import crypto from "node:crypto";
import { readFile } from "node:fs/promises";
import express from "express";

const API_KEY = process.env.ACQUIRED_DATA_API_KEY; // Dashboard -> Webhooks -> API Keys
const BASE = "https://YOUR-DOMAIN/api/v1";
// For callback_url deliveries the HMAC secret is the SHA-256 hex of your API key.
const SECRET = crypto.createHash("sha256").update(API_KEY).digest("hex");

// 1) Submit a CSV export as a bulk job
async function submitJob(csvPath) {
  const params = new URLSearchParams({
    filename: "july-absentee",
    add_ons: "dnc,phone_verification",
    callback_url: "https://yourapp.com/webhooks/acquired-data",
  });
  const res = await fetch(`${BASE}/jobs?${params}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "text/csv" },
    body: await readFile(csvPath),
  });
  const job = await res.json(); // { job_id, status, total_rows, credits_charged, ... }
  console.log("submitted", job.job_id, "credits charged:", job.credits_charged);
}

// 2) Receive + verify the completion webhook, then 3) ingest results
const app = express();
app.post(
  "/webhooks/acquired-data",
  express.raw({ type: "*/*" }), // keep the raw body for HMAC verification
  async (req, res) => {
    const sig = req.get("X-AcquiredData-Signature") ?? "";
    const expected =
      "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const valid =
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!valid) return res.status(401).end();

    res.status(200).end(); // ack fast — deliveries retry up to 3 times

    const event = JSON.parse(req.body);
    if (event.event !== "job.completed") {
      console.error("job failed:", event.error_message); // credits auto-refund
      return;
    }
    for (let page = 1; ; page++) {
      const r = await fetch(`${event.results_url}?page=${page}&per_page=1000`, {
        headers: { Authorization: `Bearer ${API_KEY}` },
      });
      const { rows } = await r.json();
      if (rows.length === 0) break;
      // await crm.upsertContacts(rows); // flat objects: primary_phone, email_1, ...
    }
  },
);
app.listen(3000);

Build in sandbox first

Every account has an environment setting, and responses tell you which one served the request ("environment": "sandbox" or "production"). Sandbox returns realistic fake data, so you can exercise the whole pipeline — submission, webhook delivery, signature verification, pagination, CRM upserts — without spending credits on real lookups. Flip to production only once the plumbing works.

Limits to design around

  • 60 requests per minute per key. Pulling results at 1,000 rows per page keeps even a 10,000-row job to a handful of requests.
  • One active trace job per account. Concurrent submissions return 429 job_in_progress with a Retry-After header. Design for a queue: hold pending lists, submit the next one when the webhook for the current one lands. This is a feature in disguise — it forces a serialized pipeline that's much easier to reason about than juggling parallel jobs.
  • Billing is up front and self-correcting. Credits are charged at submission; if a job fails, they're refunded automatically, so a failed webhook event needs no cleanup on your side.

Next steps

Grab a key and make your first call with the API getting-started guide, then go deeper on bulk jobs and webhook payloads and signatures.