Appearance
Screening Address Trace (SAT) API
Screening Address Trace finds names, aliases, address history, and counties using a full SSN or a name and date of birth. Each search saves the acquired results so you can reprocess them with different filters later. Add a webhookUrl to run the trace in the background and have the finished result sent to you; see Webhooks.
SAT is available for enabled accounts. Contact your Infocus Info representative to enable the product and configure account pricing.
You can also run SAT as part of an Instant Criminal Search by adding addressTrace to that request. The combined search uses eligible SAT names and aliases for the criminal search and returns the trace alongside the criminal results. Use the standalone endpoints below when you need an address trace by itself or want to reprocess a saved trace.
Authentication
All endpoints require an API key passed as a Bearer token in the Authorization header:
http
Authorization: Bearer your-api-keyThe key must belong to an account with SAT enabled. Saved searches can only be reprocessed by their owning account. All SAT endpoint responses include Cache-Control: no-store.
A missing or invalid key returns 403 with an empty body.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /sat/search | Execute and save an original search |
POST | /sat/search/{id}/reprocess | Reprocess the saved acquisition with new options, without a new provider search or a charge |
Create a Search
http
POST /sat/searchSend a JSON object with the following fields. The subject goes in subject, the same object an Instant Criminal Search takes. Supply either a full SSN or both first and last name with a complete birth date. subject.alias is not supported on this search.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
clientRequestId | String (≤36 chars) | No | Your own identifier for the request; echoed back in request.clientRequestId |
subject.name.firstName | String (≤120 chars) | Without SSN | Subject's first name |
subject.name.middleName | String (≤120 chars) | No | Subject's middle name |
subject.name.lastName | String (≤120 chars) | Without SSN | Subject's last name |
subject.name.suffix | String | No | Name suffix, such as Jr |
subject.birthDate | Object | Without SSN | Complete, valid birth date as numeric year, month and day; year must be at least 1 and the date cannot be in the future |
subject.ssn | String | Instead of name/birth date | Full SSN: nine digits or NNN-NN-NNNN. Send a string to preserve leading zeros |
options | Object | No | Filters described in Search Options |
userId | String (numeric) | No | Console-only user attribution; requires the protected Console credential. Omit from ordinary API integrations |
webhookUrl | String (≤2048 chars) | No | Run the trace in the background and send the finished search to this https URL. The immediate reply is 202 with status: processing. See Webhooks |
When a nonblank SSN is supplied, it is the provider search criterion. An invalid SSN produces a validation error rather than falling back to name/DOB. A nonblank birthDate supplied alongside an SSN must still be valid.
The full SSN is input-only: it is not echoed in responses or saved requests, and only its last four digits are retained on the search record. Provider SSN fields are removed from the saved acquisition.
When an SSN is supplied, only the SSN is sent to the provider; any name and date of birth you also send are kept for matching but not used to search. Without an SSN, the name (including middleName) and date of birth are sent. Unknown request fields are ignored, and a field of the wrong type returns 400 with an errors.request message.
Search Options
| Field | Type | Default | Description |
|---|---|---|---|
options.includePartialNames | Boolean | true | Include names where either first or last name matches |
options.includeNonMatchingNames | Boolean | false | Include names where neither first nor last name matches |
options.addressYears | Integer | null | null | Restrict results to addresses overlapping the past number of years, measured from acquisition date. Accepts 1–9999; null includes all available years. A person with no address inside the window is excluded from the results entirely, not just trimmed of addresses |
Name Matching
Matching ignores case and normalizes accents, punctuation, and spacing. Exact matches have matching first and last names and are always included. Partial matches have either first or last name matching. Middle names and suffixes do not determine the match category.
When both first and last name are supplied, they form the matching baseline, including for SSN searches. Otherwise, each returned person's primary name is the baseline. The first eligible name becomes the person's display name; other eligible names become aliases. A person with no eligible names, or (when addressYears is set) no address inside the window, is excluded. A name identical to the baseline is always eligible, which keeps unnamed people in SSN-only results.
Address History and Counties
The year filter includes addresses whose reported date interval overlaps the requested window through the original acquiredDate. Reprocessing later uses that same acquisition date, not the current date. Dates are understood as a year (YYYY), a year and month (YYYY-MM), an ISO date, or M/D/YYYY; a year covers that year and a year/month covers that month. Any other form, including M/YYYY and masked dates, counts as an unknown bound.
Missing or unparseable date bounds are treated as unknown; a known bound can still place an address outside the window. Addresses with both bounds unknown, or inconsistent intervals where the first date follows the last, remain visible. When a year filter is set, a person with no addresses left inside the window is excluded from the results.
Counties are resolved against the state/county catalog, ignoring case and a trailing County or Parish. A resolved address has its state and county replaced with the catalog's abbreviation and county name, and gains a countyId. Unresolved county names, and names that match more than one catalog county, are preserved without a countyId. The top-level counties list is deduplicated (ignoring case and County) from the addresses in the filtered response and sorted by state and county. Addresses without county names do not contribute to that list.
Name and Date of Birth Example
All identities and identifiers in these examples are synthetic.
bash
curl -X POST "https://api.infocusinfo.com/sat/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false,
"addressYears": 7
}
}'java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var request = HttpRequest.newBuilder(URI.create("https://api.infocusinfo.com/sat/search"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer your-api-key")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false,
"addressYears": 7
}
}
"""))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());csharp
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.infocusinfo.com/sat/search");
request.Headers.Add("Authorization", "Bearer your-api-key");
request.Content = new StringContent("""
{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false,
"addressYears": 7
}
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");http
POST https://api.infocusinfo.com/sat/search
Content-Type: application/json
Authorization: Bearer your-api-key
{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false,
"addressYears": 7
}
}SSN Example
This example uses a fictitious SSN and the default filters.
bash
curl -X POST "https://api.infocusinfo.com/sat/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"subject": {
"ssn": "000-00-0000"
}
}'java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var request = HttpRequest.newBuilder(URI.create("https://api.infocusinfo.com/sat/search"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer your-api-key")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"subject": {
"ssn": "000-00-0000"
}
}
"""))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());csharp
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.infocusinfo.com/sat/search");
request.Headers.Add("Authorization", "Bearer your-api-key");
request.Content = new StringContent("""
{
"subject": {
"ssn": "000-00-0000"
}
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");http
POST https://api.infocusinfo.com/sat/search
Content-Type: application/json
Authorization: Bearer your-api-key
{
"subject": {
"ssn": "000-00-0000"
}
}Search Response
Create, reprocess, and webhook deliveries return the same response structure. Default values are included; nullable fields are omitted when null. For example, addressYears is absent from request.options when all years are included.
Top-Level Fields
| Field | Type | Description |
|---|---|---|
request | Object | The request as saved, in the same shape you sent, except that options holds the filters this response was produced with (for a reprocess, the new ones). The SSN and webhookUrl are never included |
result | Object | The people found: offset, limit, hasMore, size and data, the same envelope an Instant Criminal Search uses. All results are returned at once, so hasMore is false |
searchId | String (numeric) | Saved search identifier; use this in reprocess URLs. A reprocess is saved as a new search with its own |
uuid | String | Search identifier that is also the webhook-id of a webhook delivery. Accepted in reprocess URLs in place of searchId. Omitted for searches saved before it was introduced |
status | String | completed, error, or processing |
message | String | Optional explanation for an empty result, processing state, or error |
acquiredDate | String | Original acquisition date in YYYY-MM-DD format; omitted when no snapshot is available |
counties | Array of objects | Distinct counties from the returned addresses |
People and Names
| Field | Type | Description |
|---|---|---|
result.data[].id | String | Identifier of the person in the acquisition: the provider's identifier, or person-N when it has none |
result.data[].name | Object | First eligible name after filtering |
result.data[].alias | Array of objects | Other eligible names |
result.data[].birthDate | Object | The most complete date of birth reported, as numeric year, month and day; parts the provider did not report are omitted, and the field is omitted when none was reported |
result.data[].birthDates | Array of objects | Every date of birth reported, in the same shape as birthDate; empty when none was reported |
result.data[].age | Integer | Age, when reported |
result.data[].addresses | Array of objects | Person's address history after filtering |
Each name and each alias entry uses the following fields:
| Field | Type | Description |
|---|---|---|
firstName, lastName | String | Name components; may be empty if unavailable |
middleName, suffix | String | Optional name components |
Addresses
| Field | Type | Description |
|---|---|---|
street | String | Street address assembled from its parts, with the unit when there is one (for example 307 Pine St, # 0); may be empty if unavailable |
city, state, postalCode | String | Optional locality fields. postalCode includes the ZIP+4 extension when the provider reports one |
county | String | County name, when available |
countyId | Integer | Resolved catalog identifier; omitted for unresolved counties |
firstReportedDate, lastReportedDate | String | Provider-reported date bounds, when available, exactly as the provider formats them (for example 2/28/1996); they may be partial dates and are not ISO dates |
Counties
| Field | Type | Description |
|---|---|---|
state | String | State; may be empty if unavailable |
county | String | County name |
countyId | Integer | Resolved catalog identifier, when available |
Completed Response Example
json
{
"request": {
"subject": {
"name": {
"firstName": "Jane",
"lastName": "Doe"
},
"birthDate": {
"year": 1980,
"month": 1,
"day": 2
},
"alias": []
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false,
"addressYears": 7
}
},
"result": {
"offset": 0,
"limit": 100,
"hasMore": false,
"size": 1,
"data": [
{
"id": "example-person-1",
"name": {
"firstName": "Jane",
"lastName": "Doe"
},
"alias": [
{
"firstName": "Jane",
"lastName": "Smith"
}
],
"addresses": [
{
"street": "123 Example Lane",
"city": "Los Angeles",
"state": "CA",
"postalCode": "90012",
"county": "Los Angeles",
"countyId": 101,
"firstReportedDate": "3/1/2020",
"lastReportedDate": "8/15/2026"
}
],
"birthDate": {
"year": 1980,
"month": 1,
"day": 2
},
"birthDates": [
{
"year": 1980,
"month": 1,
"day": 2
}
],
"age": 46
}
]
},
"searchId": "12001",
"uuid": "3f2b8c1e-9d4a-4c6b-8e1f-0a1b2c3d4e5f",
"status": "completed",
"acquiredDate": "2026-09-09",
"counties": [
{
"state": "CA",
"county": "Los Angeles",
"countyId": 101
}
]
}No Matching Records
A successful search with no matching people is completed, not an error. An empty view can also mean the selected filters excluded all acquired people; different filters may reveal saved results.
json
{
"request": {
"subject": {
"name": {
"firstName": "Jane",
"lastName": "Doe"
},
"birthDate": {
"year": 1980,
"month": 1,
"day": 2
},
"alias": []
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false
}
},
"result": {
"offset": 0,
"limit": 100,
"hasMore": false,
"size": 0,
"data": []
},
"searchId": "12002",
"uuid": "7a91d2c4-5b3e-4f08-9c1d-2e6f8a0b3c47",
"status": "completed",
"message": "No matching address trace records for these filters.",
"acquiredDate": "2026-09-09",
"counties": []
}Reprocess a Saved Search
POST /sat/search/{id}/reprocess reprocesses the original saved acquisition with new options. It is saved as a new search with its own searchId and uuid, so it can be delivered to a webhook like any other search.
| Field | Type | Required | Description |
|---|---|---|---|
options | Object | No | The filters for this run, described in Search Options. Omit to use the defaults |
webhookUrl | String (≤2048 chars) | No | Run the reprocess in the background and send the finished search to this https URL. The immediate reply is 202 with status: processing. See Webhooks |
Omitted option fields use the defaults, rather than inheriting the original or most recent filters. Sending {} includes partial names, excludes nonmatching names, and includes all available years. An empty body returns 400.
The response has the new searchId and uuid, the original acquisition date, and the filters it was produced with in request.options. The saved acquisition and the original search are not modified. addressYears is validated before the search is looked up, so an invalid value returns 400 even for an unknown search. Reprocessing a search that is still processing, or ended in error, returns 400 with an error under searchId.
bash
curl -X POST "https://api.infocusinfo.com/sat/search/12001/reprocess" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"options": {
"includePartialNames": true,
"includeNonMatchingNames": true,
"addressYears": null
}
}'java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var request = HttpRequest.newBuilder(URI.create("https://api.infocusinfo.com/sat/search/12001/reprocess"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer your-api-key")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"options": {
"includePartialNames": true,
"includeNonMatchingNames": true,
"addressYears": null
}
}
"""))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());csharp
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.infocusinfo.com/sat/search/12001/reprocess");
request.Headers.Add("Authorization", "Bearer your-api-key");
request.Content = new StringContent("""
{
"options": {
"includePartialNames": true,
"includeNonMatchingNames": true,
"addressYears": null
}
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");http
POST https://api.infocusinfo.com/sat/search/12001/reprocess
Content-Type: application/json
Authorization: Bearer your-api-key
{
"options": {
"includePartialNames": true,
"includeNonMatchingNames": true,
"addressYears": null
}
}All acquired names and addresses are saved before these local filters are applied. Reprocessing can reveal previously excluded data, but cannot obtain records the provider did not return in the original search. It makes no provider call and creates no charge.
Webhooks
Add webhookUrl to run the address trace in the background. The API saves the search and replies right away with HTTP 202 and status: processing, then sends the finished search to your URL. Without it, the request waits and returns the result as described above. The webhook carries the original-filter response; use POST /sat/search/{id}/reprocess afterwards to apply other filters. A reprocess takes a webhookUrl of its own.
webhookUrl must be an absolute https URL, up to 2048 characters, without embedded credentials, whose host resolves to a public address (loopback, private, link-local, multicast, carrier-grade NAT, and IPv6 unique-local addresses are refused, as is a host that cannot be resolved). An invalid URL returns 400 with an error under webhookUrl. The URL is kept only to deliver the webhook; it is not echoed in responses or stored in the saved request.
Example
bash
curl -X POST "https://api.infocusinfo.com/sat/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"webhookUrl": "https://example.com/hooks/sat"
}'java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var request = HttpRequest.newBuilder(URI.create("https://api.infocusinfo.com/sat/search"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer your-api-key")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"webhookUrl": "https://example.com/hooks/sat"
}
"""))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());csharp
using System.Net.Http;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.infocusinfo.com/sat/search");
request.Headers.Add("Authorization", "Bearer your-api-key");
request.Content = new StringContent("""
{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"webhookUrl": "https://example.com/hooks/sat"
}
""", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");http
POST https://api.infocusinfo.com/sat/search
Content-Type: application/json
Authorization: Bearer your-api-key
{
"subject": {
"name": { "firstName": "Jane", "lastName": "Doe" },
"birthDate": { "year": 1980, "month": 1, "day": 2 }
},
"webhookUrl": "https://example.com/hooks/sat"
}The reply is the saved search in the processing state. Keep its uuid:
json
{
"request": {
"subject": {
"name": {
"firstName": "Jane",
"lastName": "Doe"
},
"birthDate": {
"year": 1980,
"month": 1,
"day": 2
},
"alias": []
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false
}
},
"result": {
"offset": 0,
"limit": 100,
"hasMore": false,
"size": 0,
"data": []
},
"searchId": "12001",
"uuid": "3f2b8c1e-9d4a-4c6b-8e1f-0a1b2c3d4e5f",
"status": "processing",
"message": "The address trace is processing.",
"counties": []
}Placement problems (validation, product access, account checks) are reported immediately with the usual 400, 403, 404, or 503, before anything is accepted.
When the search finishes, the API sends a POST to your URL with the same body as the create response, including status (completed or error). The request follows the Standard Webhooks specification and carries these headers:
| Header | Description |
|---|---|
webhook-id | The search's uuid. Identical on every retry, so use it to de-duplicate |
webhook-timestamp | Unix time in seconds of the attempt |
webhook-signature | v1, plus the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the API key that placed the search |
Respond with any 2xx status to acknowledge. Other responses, timeouts (10 seconds), and connection errors are retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours, 12 hours, and 24 hours, after which the API stops. Delivery is at least once, and redirects are not followed. If the API key that placed the search has since been revoked or has expired, the delivery cannot be signed and is abandoned without retries. See Verifying a webhook for signature code samples.
A search that cannot be completed is delivered with status: error. A search that was running when the API restarted cannot be resumed (the full SSN is never stored); if it is still processing 15 minutes after it was created, it is marked error and that status is delivered. A search without a webhook that is interrupted this way is not swept and may stay processing. Searches are never re-sent to a provider automatically. Billing is unchanged: a search is billed once, when it completes.
Validation and Errors
HTTP Response Codes
| Code | Description |
|---|---|
200 | A search response returned. Inspect status to distinguish completion from processing or failure |
202 | The search was accepted for background processing because the request included a webhookUrl. The result is sent to the webhook |
400 | Invalid fields or malformed JSON; includes an errors object keyed by field or request category |
403 | Missing or invalid API key, SAT is not enabled for the account on search creation, or console-only attribution is not authorized |
404 | SAT product or saved search not found, invalid search ID, or search is not owned by the account |
500 | Unexpected request failure |
503 | SAT setup is incomplete or the search service is not configured |
Errors other than field validation use these keys in the errors object:
| Key | Message | Code |
|---|---|---|
request | Malformed request. | 400 (invalid JSON or a field of the wrong type) |
userId | Invalid user ID. | 400 |
access | Address trace is not available for this account. | 403 |
search | Not found. | 404 |
search | Address trace is not configured. (or: Address trace setup is incomplete. Contact your administrator.) | 503 |
search | The request could not be completed. | 500 |
For example, an invalid DOB in a name/DOB request returns HTTP 400:
json
{
"errors": {
"birthDate": "Enter a valid complete date of birth (year, month and day)."
}
}An invalid addressYears value on either creation or reprocessing returns HTTP 400:
json
{
"errors": {
"addressYears": "Address history years must be between 1 and 9999, or blank for all."
}
}Use null or omit addressYears for all years; do not send a blank string.
Search-Level Failure
A provider failure can return HTTP 200 with a persisted search whose status is error. Check the status even when the HTTP request succeeds:
json
{
"request": {
"subject": {
"name": {
"firstName": "Jane",
"lastName": "Doe"
},
"birthDate": {
"year": 1980,
"month": 1,
"day": 2
},
"alias": []
},
"options": {
"includePartialNames": true,
"includeNonMatchingNames": false
}
},
"result": {
"offset": 0,
"limit": 100,
"hasMore": false,
"size": 0,
"data": []
},
"searchId": "12003",
"uuid": "c04e6b79-1d2a-4a35-b8f0-91d7e3a5c612",
"status": "error",
"message": "The address trace could not be completed. No automatic retry was made.",
"counties": []
}Billing and Retries
Each completed original search is billed once at the account's price when the order is written (its active price override, otherwise the product price), including searches with no matching records. Billing delays do not discard completed results. Reprocessing a saved search is free.
Without a webhookUrl, search creation is synchronous, with a 30-second provider timeout and no automatic provider retry; with one, the same limits apply to the background request. There is no request-level idempotency key: submitting another POST /sat/search creates another original search and can incur another charge.
If a request is interrupted, do not automatically resubmit it; the provider may already have executed the search. If you received a searchId, the search was placed, and its result is sent to your webhook if you set one.