Data API · version 1 · for Pro and Business
The trustee sale data API
The TrusteeSaleData data API is read-only JSON over the same trustee sales the site shows, from published notices of trustee’s sale and trustee’s sale records alike: search them, fetch one by its id, and keep a copy of your markets in step. It comes with Pro and Business, uses keys you create on your account page, and shares your plan’s monthly records with CSV export; through the API a sale counts once a month, however often you fetch it.
- Pro
- 3,000
- records a month, 60 requests a minute
- Business
- 10,000
- records a month, 120 requests a minute
- Page size
- Up to 200
- sales a request
- Format
- JSON
- OpenAPI 3.1
Every plan starts with a 7-day free trial, and the API opens with the first payment. To use it the day you sign up, start your plan from your account page and the trial ends early.
Quick start
- Subscribe to Pro or Business. The API opens with the first payment; during the free trial, the Start <your plan> now button under Subscription on your account page ends the trial and opens it today.
- Create a key under Data API on your account page, and copy it when it is shown: it is shown once.
- Keep it in an environment variable on the machine that calls the API, here
TRUSTEESALEDATA_API_KEY, and send it in theAuthorizationheader:
curl https://www.trusteesaledata.com/api/v1/me \
-H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"curl "https://www.trusteesaledata.com/api/v1/sales?state=AZ&status=upcoming&limit=100" \
-H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"Authentication
- Every request carries
Authorization: Bearer <key>, and nothing else is accepted: not your website sign-in (the API ignores cookies), not anX-API-Keyheader, not a query parameter. - A key is
tsd_api_followed by 46 letters and digits, matching^tsd_api_[0-9A-Za-z]{46}$: give that pattern to your secret scanner. Its last six characters are a checksum, so a mistyped or cut-short key is refused at once with 401 invalid_key. - Never put a key in a web address. Addresses are written to access logs on their way in, so a key found in one, in any parameter or in the path, encoded or not, is answered with 400 key_in_url and revoked, and we email you to say so.
- Keys belong on servers. The data endpoints send no CORS headers, so a web page cannot call them; a key in front-end code, a mobile app or the cells of a shared spreadsheet is a published key (the Google Sheets example below keeps it in the script’s properties instead).
- A connected app is not a key. ChatGPT and other apps you connect under Connected apps on your account page use tokens of their own; one sent to the data API is refused with 401 invalid_key, saying what it is. Create a key for your own code.
- Pro holds 2 live keys at once and Business 5; up to 10 can be created in a day. A key unused for 180 days stops working.
- Every answer from a data endpoint, errors included, carries an
X-Request-Id, which is an error’srequest_idtoo. It is all we need to look into a request: never send us a key.
Base URL, format and versions
Every endpoint is under https://www.trusteesaledata.com/api/v1: the www address, over HTTPS, with no trailing slash (an address ending in a slash is answered with a redirect to the one without).
- JSON only, UTF-8. Dates are
YYYY-MM-DD, times ISO 8601 in UTC, money a number of US dollars, and a value the record does not state isnull, never left out. - No CSV. The API has no CSV format and no
formatparameter. CSV export is part of the website, from a search, signed in; it shares the same monthly allowance and does not take API keys (a request to it carrying anAuthorizationheader is refused). - Version 1 changes only by addition: new fields, endpoints and optional parameters may appear at any time, so ignore what you do not know. A change that would break a client comes as
/api/v2, announced by email at least 30 days ahead. - The OpenAPI 3.1 description is at https://www.trusteesaledata.com/api/v1/openapi.json, for Postman, Insomnia or a client generator. It and
https://www.trusteesaledata.com/api/v1need no key.
Plans and limits
| Limit | Starter | Pro | Business |
|---|---|---|---|
| Price | $29 a month | $49 a month | $99 a month |
| The data API | — | Included | Included |
| Records a monthBy CSV export and the API together. Through the API, a sale counts once a month. | — | 3,000 | 10,000 |
| New records through the APIIn any 24 hours | — | 1,000 | 2,500 |
| Requests a minuteEvery key together | — | 60 | 120 |
| Requests a dayUTC, from midnight | — | 5,000 | 10,000 |
| Requests at once | — | 2 | 3 |
| Live keys | — | 2 | 5 |
| Sales per page100 unless asked | — | Up to 200 | Up to 200 |
During the free trial
The API opens with the first payment, as CSV export does: during the 7-day free trial no key can be created, and a key’s requests are answered 403 trial_limit. To use it today, end the trial early: Start Pro now (or Business), under Subscription on your account page, takes the first payment at checkout, opens CSV export and the API as soon as it goes through, and cancels the trial once the payment is confirmed, so you are never charged twice. Your saved searches and alerts carry on.
When the plan changes
A new plan’s limits apply from the next request. If a payment fails, the API pauses (403 payment_past_due) until it goes through, and your keys are kept. On a plan without the API, your keys stay listed on your account page, answered 403 forbidden_plan, until you revoke them or move back.
What counts against your allowance
- Records a month are one allowance, shared with CSV export: Pro 3,000, Business 10,000. A CSV row is a record, and through the API a sale is a record the first time any of your keys fetches it in the calendar month (UTC). After that it is free for the rest of the month, even once the allowance is spent: pulling the same markets every day costs each sale once.
- New records in any 24 hours, through the API: Pro 1,000, Business 2,500. A ceiling that keeps a leaked key from taking a month in a morning; sales you fetched already do not count.
- Free:
/me,/states,/states/{state}/counties,/sales/count,/api/v1andopenapi.json, every error, and any sale already fetched this month. Requests are never charged, only rate-limited. - A page that would go past the allowance is cut short, not refused. The sales that fit are served, in order, with
truncated: trueand anext_cursorthat resumes at the first one held back. Only when not one sale on a page can be served is the answer 429 quota_exceeded, whosedetails.limitsays which allowance (monthorday) and whoseRetry-Aftersays when it comes back: 00:00 UTC on the 1st, or when the oldest new record leaves the 24 hours. - “The same sale” is the sale itself, the property and the loan being foreclosed, not its id: a newer notice of the sale moves its id but does not count it again. Now and then a re-notice for a new date, or a regrouping of the records behind a sale, counts it twice in one month.
- Records through the API do not touch the site’s own daily limit on viewing full records, so a nightly sync never locks you out of the site.
Rate limits and headers
- Requests are counted per account, every key together: a minute (sliding), a day (UTC) and at once, as the table above sets out. Over any of them, the answer is 429 rate_limited with
details.window(minute,dayorin_flight). WaitRetry-Afterseconds and repeat the request. - The API answers at most 4 requests at once across every account; past that, and when the database is slow, the answer is 503 busy, with a
Retry-After. - Once 20 requests from one address have sent keys that do not work within 10 minutes, further bad keys from it get 429 (
failed_auth) instead of 401. A working key from the same address is never refused for its neighbours’ mistakes: offices and automation platforms share addresses. - A key whose plan does not open the API gets 10 requests a minute, 500 a day and 1 at once: enough to read
/meand the reason.
Every response to a key that works carries the RateLimit headers, errors included; sales and /me add the allowance’s. The same numbers are in the body’s usage, for tools that hide headers. Answers given before the key is read carry only X-Request-Id: a 401, a key sent in the address, an unknown path or method, too many failed keys from one address, and a 503 when the API is paused or full.
| Header | What it says |
|---|---|
X-Request-Id | This response's id; quote it to support. The same as error.request_id. |
RateLimit-Limit | Requests allowed in the sliding minute, for the account across all its keys. The RateLimit headers come on every response to a key that works, errors included, and on none sent before the key is read: a 401, a key_in_url 400, an unknown path or method, a failed_auth 429, and a 503 when the API is paused or full. |
RateLimit-Remaining | Requests left in the sliding minute. |
RateLimit-Reset | Seconds until the oldest request in the minute leaves it (0 when none is counted). |
RateLimit-Policy | Every request window: requests a minute, then a UTC day (60;w=60, 5000;w=86400). |
X-Quota-Month-Limit | Records a month, by CSV export and the API together. |
X-Quota-Month-Used | Records used this month: CSV rows plus distinct sales through the API. |
X-Quota-Month-Remaining | Records left this month. |
X-Quota-Month-Reset | When the month starts again: 00:00 UTC on the 1st. |
X-Quota-Day-Limit | New records the API may count in any 24 hours. |
X-Quota-Day-Remaining | New records left in the last 24 hours. |
X-Records-Counted | Sales this response counted: those not already fetched this month. |
Retry-After | On a 429 or 503: seconds to wait before repeating the request. |
Endpoints
Every endpoint answers GET and nothing else (405, HEAD included, so nothing is spent on a body nobody reads), and an address under /api/v1 that is not one of these is a JSON 404. Parameters are strict: one the endpoint does not take is 400 unknown_parameter rather than ignored, because a misspelt filter that is quietly dropped returns a different list from the one asked for.
GET /sales
Search the sales your plan opens, a page at a time. Metered: each sale counts once a month. Upcoming sales, soonest first, unless you ask otherwise; total comes on the first page only.
| Parameter | Accepts | What it does |
|---|---|---|
q | Text, 1 to 100 characters | Text to find in a sale's address, city, county, parcel number (APN) or trustee sale number. Never people: borrowers are not searched. |
state | A two-letter code | Two-letter state code: one /states lists, or any state a sale is in. |
county | Text, 1 to 80 characters | A county, spelled exactly as /states/{state}/counties or a sale's county gives it. Needs state. |
city | Text, 1 to 60 characters | A city in the state, in any case: Phoenix or phoenix. Needs state. |
status | One of upcoming, past, all, postponed; upcoming when left out | upcoming (from today), past, all, or postponed (sales the trustee has postponed). |
sale_date_from | A date, YYYY-MM-DD | Sales on or after this date (YYYY-MM-DD), after any postponement. The search page calls it from. |
sale_date_to | A date, YYYY-MM-DD | Sales on or before this date (YYYY-MM-DD), after any postponement. The search page calls it to. |
first_seen_after | An ISO 8601 date and time with its offset, or a date | Sales we first saw after this moment (ISO 8601 with an offset, or a date). For incremental sync, prefer sort=first_seen and its cursor. |
min_unpaid_balance | A number of US dollars, 0 or more | Unpaid balance at least this many US dollars. The search page calls it minBalance. |
max_unpaid_balance | A number of US dollars, 0 or more | Unpaid balance at most this many US dollars. The search page calls it maxBalance. |
sort | One of sale_date, -sale_date, first_seen, -first_seen, unpaid_balance, -unpaid_balance | sale_date (default; -sale_date for past), first_seen or -first_seen (when we first saw it: the order for keeping a copy in sync), unpaid_balance or -unpaid_balance. Nulls sort last. |
limit | A whole number, 1 to 200; 100 when left out | Sales per page, 1 to 200. |
cursor | As it came, up to 512 characters | The next_cursor of the previous page, as it came. Valid only with the same filters and sort. |
curl "https://www.trusteesaledata.com/api/v1/sales?state=AZ&status=upcoming&limit=100" \
-H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"{
"object": "list",
"data": [
{
"id": "00000000decafbad",
"address": "123 Example St",
"city": "Phoenix",
"county": "Maricopa",
"state": "AZ",
"zip": "85000",
"apn": "000-00-000",
"sale_date": "2026-11-04",
"sale_time": "10:00 AM",
"sale_status": "Scheduled",
"postponed_to": null,
"unpaid_balance": 184250.12,
"opening_bid": null,
"original_loan_amount": 210000,
"borrower": "Jane Example",
"beneficiary": "Example Bank, N.A.",
"trustee": "Example Trustee Services, LLC",
"sale_line": "(555) 010-0000",
"trustee_sale_number": "TS-0000-00000",
"sale_location": "At the main entrance of the Example County Courthouse",
"first_seen_at": "2026-10-11T10:10:12.000Z",
"last_seen_at": "2026-10-11T10:10:12.000Z",
"url": "https://www.trusteesaledata.com/listings/00000000decafbad",
"other_ids": [
"00000000feedface"
],
"property_type": null,
"other_noticed_dates": [],
"sale_date_unverified": false
}
],
"has_more": true,
"next_cursor": "eyJ2IjoxLCJzIjoic2FsZV9kYXRlIn0.ZXhhbXBsZS1jdXJzb3ItdGFn",
"truncated": false,
"total": 1210,
"filters": {
"state": "AZ",
"status": "upcoming",
"sort": "sale_date"
},
"usage": {
"records_counted": 1,
"month": {
"limit": 3000,
"csv": 120,
"api": 721,
"used": 841,
"remaining": 2159,
"resets_at": "2026-11-01T00:00:00Z"
},
"day": {
"limit": 1000,
"used": 211,
"remaining": 789,
"frees_at": "2026-10-13T09:12:44.512Z"
}
},
"data_as_of": "2026-10-12"
}GET /sales/count
How many sales match, for free. It takes the list’s filters except q, and no page: a count narrowed to one address or parcel would say whether that sale exists without fetching it.
| Parameter | Accepts | What it does |
|---|---|---|
state | A two-letter code | Two-letter state code: one /states lists, or any state a sale is in. |
county | Text, 1 to 80 characters | A county, spelled exactly as /states/{state}/counties or a sale's county gives it. Needs state. |
city | Text, 1 to 60 characters | A city in the state, in any case: Phoenix or phoenix. Needs state. |
status | One of upcoming, past, all, postponed; upcoming when left out | upcoming (from today), past, all, or postponed (sales the trustee has postponed). |
sale_date_from | A date, YYYY-MM-DD | Sales on or after this date (YYYY-MM-DD), after any postponement. The search page calls it from. |
sale_date_to | A date, YYYY-MM-DD | Sales on or before this date (YYYY-MM-DD), after any postponement. The search page calls it to. |
first_seen_after | An ISO 8601 date and time with its offset, or a date | Sales we first saw after this moment (ISO 8601 with an offset, or a date). For incremental sync, prefer sort=first_seen and its cursor. |
min_unpaid_balance | A number of US dollars, 0 or more | Unpaid balance at least this many US dollars. The search page calls it minBalance. |
max_unpaid_balance | A number of US dollars, 0 or more | Unpaid balance at most this many US dollars. The search page calls it maxBalance. |
{
"object": "count",
"total": 640,
"filters": {
"state": "AZ",
"status": "upcoming"
},
"data_as_of": "2026-10-12"
}GET /sales/{id}
One sale, by any id it has had. A sale’s id is the public id of the notice or sale record now representing it, sixteen hexadecimal characters, and it moves when a newer notice of the sale arrives; an id the sale no longer carries is answered with the sale as it is now, id_changed: true, and the id you asked for as requested_id. An id whose sale is no longer listed, because it was withdrawn, cancelled or otherwise taken off the list, is 410 gone; an id we never issued is 404. Metered.
{
"object": "sale",
"data": {
"id": "00000000decafbad",
"address": "123 Example St",
"city": "Phoenix",
"county": "Maricopa",
"state": "AZ",
"zip": "85000",
"apn": "000-00-000",
"sale_date": "2026-11-04",
"sale_time": "10:00 AM",
"sale_status": "Scheduled",
"postponed_to": null,
"unpaid_balance": 184250.12,
"opening_bid": null,
"original_loan_amount": 210000,
"borrower": "Jane Example",
"beneficiary": "Example Bank, N.A.",
"trustee": "Example Trustee Services, LLC",
"sale_line": "(555) 010-0000",
"trustee_sale_number": "TS-0000-00000",
"sale_location": "At the main entrance of the Example County Courthouse",
"first_seen_at": "2026-10-11T10:10:12.000Z",
"last_seen_at": "2026-10-11T10:10:12.000Z",
"url": "https://www.trusteesaledata.com/listings/00000000decafbad",
"other_ids": [
"00000000feedface"
],
"property_type": null,
"other_noticed_dates": [],
"sale_date_unverified": false
},
"requested_id": "00000000feedface",
"id_changed": true,
"usage": {
"records_counted": 0,
"month": {
"limit": 3000,
"csv": 120,
"api": 721,
"used": 841,
"remaining": 2159,
"resets_at": "2026-11-01T00:00:00Z"
},
"day": {
"limit": 1000,
"used": 211,
"remaining": 789,
"frees_at": "2026-10-13T09:12:44.512Z"
}
},
"data_as_of": "2026-10-12"
}GET /states
Every state we cover, the busiest first, with its counts. Free.
| Field | Type | What it holds |
|---|---|---|
state | string | Two-letter state code. |
name | string | The state's name. |
upcoming_sales | integer | Sales from today on. |
next_30_days | integer | Sales in the next 30 days. |
held_last_90_days | integer | Sales held in the last 90 days. |
next_sale_date | string (date), or null | The next sale's date. |
counties | integer | Counties with sales: the ones /states/{state}/counties lists. |
url | string (uri) | The state's page on the site. |
{
"object": "list",
"data": [
{
"state": "AZ",
"name": "Arizona",
"upcoming_sales": 1200,
"next_30_days": 480,
"held_last_90_days": 610,
"next_sale_date": "2026-10-13",
"counties": 15,
"url": "https://www.trusteesaledata.com/trustee-sales/arizona"
}
],
"data_as_of": "2026-10-12"
}GET /states/{state}/counties
Every county in the state that holds a sale, the busiest first, with the exact spelling the county filter wants. A county the site has no page for has a null url. Free. A state we neither cover nor hold a sale in is a 404.
| Field | Type | What it holds |
|---|---|---|
county | string | The county, spelled exactly as the county filter wants it. |
state | string | Two-letter state code. |
upcoming_sales | integer | Sales from today on. |
next_30_days | integer | Sales in the next 30 days. |
held_last_90_days | integer | Sales held in the last 90 days. |
next_sale_date | string (date), or null | The next sale's date. |
url | string (uri), or null | The county's page on the site; null for a county that has none. |
{
"object": "list",
"state": "AZ",
"data": [
{
"county": "Maricopa",
"state": "AZ",
"upcoming_sales": 800,
"next_30_days": 300,
"held_last_90_days": 380,
"next_sale_date": "2026-10-15",
"url": "https://www.trusteesaledata.com/trustee-sales/arizona/maricopa"
}
],
"data_as_of": "2026-10-12"
}GET /me
The key’s account: its plan, what the API opens for it and, when that is nothing, why; its limits, its usage and the key itself, by name and last four characters. It answers any key that works, whatever the plan, and carries no email address. Free.
{
"object": "account",
"plan": "Pro",
"trial": false,
"status": "active",
"api": {
"keys": true,
"records": true,
"reason": null
},
"limits": {
"keys": 2,
"requests_per_minute": 60,
"requests_per_day": 5000,
"in_flight": 2,
"records_per_day": 1000,
"records_per_month": 3000,
"max_page_size": 200
},
"usage": {
"month": {
"limit": 3000,
"csv": 120,
"api": 721,
"used": 841,
"remaining": 2159,
"resets_at": "2026-11-01T00:00:00Z"
},
"day": {
"limit": 1000,
"used": 211,
"remaining": 789,
"frees_at": "2026-10-13T09:12:44.512Z"
}
},
"keys": {
"live": 1,
"max": 2
},
"key": {
"name": "CRM sync",
"last4": "a1B2",
"created_at": "2026-10-12T14:05:00.000Z",
"last_used_at": "2026-10-12T14:20:00.000Z"
}
}GET / · GET /openapi.json
https://www.trusteesaledata.com/api/v1 lists the endpoints, and /openapi.json describes them in OpenAPI 3.1. Neither needs a key, and both may be read from any origin: they carry no one’s data.
The sale object
A sale carries the CSV export’s fields, under the same names and in the same order, then when we first and last saw it, its record page, its other ids, the kind of property, its other noticed dates and whether its sale date needs checking. Every field is always present. The text of the notice is not one of them, as it is not part of CSV export: it stays on the sale’s record page (url), for plans that include it.
| Field | Type | What it holds |
|---|---|---|
id | string | The sale's id: the public id of the notice or sale record now representing it. It moves to a newer notice of the sale when one arrives; any id it has had still finds it. |
address | string, or null | The property's address as published. |
city | string, or null | The property's city. |
county | string, or null | The county, spelled as the county filter wants it. |
state | string | Two-letter state code. |
zip | string, or null | The property's ZIP code. |
apn | string, or null | The parcel number (APN) as published. |
sale_date | string (date), or null | The date the sale is set for, after any postponement. |
sale_time | string, or null | The time of the sale as published. |
sale_status | string, or null | The sale's status: Scheduled, Postponed or Sold, where one is given. |
postponed_to | string (date), or null | The date a postponement moved the sale to. |
unpaid_balance | number, or null | The unpaid balance the notice states, in US dollars. |
opening_bid | number, or null | The opening bid in US dollars, where one is stated. |
original_loan_amount | number, or null | The loan's original principal as the notice states it, in US dollars. Not the balance. |
borrower | string, or null | The borrower or owner the notice names. |
beneficiary | string, or null | Who the debt is owed to: the lender or its successor. |
trustee | string, or null | The trustee conducting the sale. |
sale_line | string, or null | The trustee's sale information line. |
trustee_sale_number | string, or null | The trustee's sale number. |
sale_location | string, or null | Where the sale is held. |
first_seen_at | string (date-time) | When we first saw the sale: the order for incremental sync (sort=first_seen). |
last_seen_at | string (date-time) | When it was last seen in a published notice or a trustee's sale record. |
url | string (uri) | The sale's record page on the site. |
other_ids | array of strings | The ids of the sale's other notices and sale records, newest first. An id you hold that is here is this sale. |
property_type | string, or null | The kind of property, where it is stated. |
other_noticed_dates | array of strings | The sale's other noticed dates, when one loan was noticed for more than one (YYYY-MM-DD, ascending). |
sale_date_unverified | boolean | The printed sale date is not a legal sale day; confirm it with the trustee. |
Paging, and keeping a copy in sync
Lists are paged by cursor. Ask for up to 200 sales with limit, then pass each page’s next_cursor back as cursor, with the same filters and sort, until it is null. A cursor resumes exactly after the last sale it was given, so sales added or dropped by a refresh while you walk never make it skip or repeat one (a sale whose date or id changes mid-walk, postponed or noticed again, can move past the cursor; the weekly re-pull below catches it). A cursor sent with other filters is 400 invalid_cursor. You may change limit between pages.
Keeping a copy in step
- Once: walk your markets’ upcoming sales (the default
status) in the order we first saw them, to the end, keeping each sale by itsid:/sales?state=AZ&sort=first_seen. - Every day, or as often as every hour (new sales are added through the day): ask for what is new since,
first_seen_afterset a few minutes before the newestfirst_seen_atyou hold. A sale you already have comes back free. - Every week: pull your markets’ upcoming sales again and replace your copy of them. Sales are postponed, often more than once, and their dates, balances and statuses change; sales are withdrawn or cancelled and leave the list; and now and then a sale joins with an older
first_seen_at. Sales fetched this month cost nothing to fetch again.
Ids can change. When a newer notice or sale record of the same sale takes it over, the sale’s id moves to it. other_ids lists the ids of its other notices and sale records, so an id you hold that appears there is this sale, and /sales/{id} still finds a sale by an id it used to have. An id whose sale has left the list is 410 gone: remove it from your copy. Sales, counts and coverage carry data_as_of, the date of the newest data behind them.
Errors
Every error has the same shape, so a client needs one branch: a code to act on, a message for people, the param at fault when there is one, details where they help, a doc_url to its row below and the request_id. On a 429 or a 503, wait Retry-After seconds and repeat the request, except on quota_exceeded, which means stop until the allowance comes back.
{
"error": {
"code": "invalid_parameter",
"message": "sale_date_from must be a real date written YYYY-MM-DD, such as 2026-10-31.",
"param": "sale_date_from",
"doc_url": "https://www.trusteesaledata.com/trustee-sale-data-api#error-invalid_parameter",
"request_id": "req_0123456789abcdef01234567"
}
}| Code | Status | When | What to do |
|---|---|---|---|
invalid_parameter | 400 | A query parameter or path segment has a value the API does not accept, or one is sent twice. param names it. | Fix the value; the message says what is accepted. |
unknown_parameter | 400 | A query parameter this endpoint does not take. Parameters are strict, so a misspelt filter is an error rather than ignored. | Check the spelling against the endpoint's parameter list. |
invalid_cursor | 400 | The cursor is damaged, was issued for different filters or sort, or was signed under a key the site no longer uses. | Repeat the request with the original parameters, or start again without a cursor. |
key_in_url | 400 | An API key appeared in the web address. Addresses are written to access logs, so a key found in one is revoked. | Create a new key and send it only in the Authorization header. |
unauthorized | 401 | No Authorization: Bearer header was sent. A website sign-in cookie does not authenticate the API. | Send Authorization: Bearer <key>. |
invalid_key | 401 | The key is malformed, unknown, revoked, expired after 180 days unused, or belongs to a closed account; details.reason says which. | Check that the key was copied whole, or create a new one on the account page. |
forbidden_plan | 403 | The account's plan does not include the data API, or not this part of it. | See details.upgrade_url for the plans that include it. |
trial_limit | 403 | The account is in its free trial, and the data API opens with the first payment. | Start the plan now from the account page (details.start_now_url), or wait for the trial to end. |
payment_past_due | 403 | The last payment did not go through, so the API is paused. The keys are kept. | Update the card from the account page; the API resumes when the payment goes through. |
plan_inactive | 403 | The account's plan is not active, so the API is closed. | Contact us if this is unexpected. |
not_found | 404 | No sale, state or endpoint by that id or path. | Search again: a sale's id can change when a newer record takes it over. |
method_not_allowed | 405 | The data API is read-only and answers GET only. | Use GET. |
gone | 410 | The id belonged to a sale that is no longer listed: withdrawn, cancelled or otherwise off the list. | Remove it from your copy. |
rate_limited | 429 | Too many requests: per minute, per day, at once, or from an address sending keys that do not work. details.window says which. | Wait for Retry-After seconds, then repeat the request. |
quota_exceeded | 429 | The records allowance is spent and every sale on the page would be new: the month's (shared with CSV export) or the 24-hour ceiling. details.limit says which. | Stop, and resume after Retry-After seconds. Sales already fetched this month can still be fetched. |
server_error | 500 | Something went wrong on our side. | Retry later; quote the request_id if it keeps happening. |
busy | 503 | The API is answering as many requests as it can, or the database took too long. | Wait for Retry-After seconds, then repeat the request. |
api_disabled | 503 | The API is paused for maintenance. Keys and allowances are kept. | Retry after Retry-After seconds. |
Examples
Each walks every page of a search, waits out a 429 or a 503 for as long as Retry-After says, and stops when the allowance is spent. Each reads the key from TRUSTEESALEDATA_API_KEY.
curl "https://www.trusteesaledata.com/api/v1/sales?state=AZ&status=upcoming&limit=100" \
-H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"import os, time, requests
BASE = "https://www.trusteesaledata.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["TRUSTEESALEDATA_API_KEY"]
def sales(**filters):
"""Every sale matching the filters, a page at a time."""
params = {"limit": 200, **filters}
while True:
response = session.get(f"{BASE}/sales", params=params, timeout=60)
if response.status_code in (429, 503):
error = response.json()["error"]
if error["code"] == "quota_exceeded":
raise SystemExit(error["message"]) # the allowance is spent: stop
time.sleep(int(response.headers.get("Retry-After", "5")))
continue
response.raise_for_status()
page = response.json()
yield from page["data"]
if not page["next_cursor"]:
return
params["cursor"] = page["next_cursor"]
for sale in sales(state="AZ", status="upcoming"):
print(sale["id"], sale["sale_date"], sale["county"], sale["unpaid_balance"])// Node 18 or later, from a server or a scheduled job: never a web page.
const BASE = "https://www.trusteesaledata.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.TRUSTEESALEDATA_API_KEY}` };
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
async function* sales(filters) {
const params = new URLSearchParams({ limit: "200", ...filters });
while (true) {
const response = await fetch(`${BASE}/sales?${params}`, { headers });
const body = await response.json();
if (response.status === 429 || response.status === 503) {
if (body.error.code === "quota_exceeded") throw new Error(body.error.message);
await sleep(Number(response.headers.get("retry-after") ?? 5));
continue;
}
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
yield* body.data;
if (!body.next_cursor) return;
params.set("cursor", body.next_cursor);
}
}
for await (const sale of sales({ state: "AZ", status: "upcoming" })) {
console.log(sale.id, sale.sale_date, sale.county, sale.unpaid_balance);
}// Google Sheets: Extensions > Apps Script. Save the key once, under
// Project Settings > Script properties, as TRUSTEESALEDATA_API_KEY: it stays in the
// script, out of the sheet. Run importSales, or give it a daily trigger.
const BASE = "https://www.trusteesaledata.com/api/v1";
const FIELDS = ["id", "state", "county", "city", "address", "apn", "sale_date", "sale_status", "unpaid_balance", "trustee", "url"];
function importSales() {
const key = PropertiesService.getScriptProperties().getProperty("TRUSTEESALEDATA_API_KEY");
const book = SpreadsheetApp.getActiveSpreadsheet();
const sheet = book.getSheetByName("Sales") || book.insertSheet("Sales");
const rows = [FIELDS];
let cursor = "";
while (true) {
const query = "state=AZ&status=upcoming&limit=200" + (cursor ? "&cursor=" + encodeURIComponent(cursor) : "");
const response = UrlFetchApp.fetch(BASE + "/sales?" + query, {
headers: { Authorization: "Bearer " + key },
muteHttpExceptions: true,
});
const status = response.getResponseCode();
const body = JSON.parse(response.getContentText());
if (status === 429 || status === 503) {
if (body.error.code === "quota_exceeded") break; // the allowance is spent: keep what came
const headers = response.getHeaders(); // named as the server sent them: lower case
Utilities.sleep(1000 * Number(headers["retry-after"] || headers["Retry-After"] || 5));
continue;
}
if (status !== 200) throw new Error(body.error.code + ": " + body.error.message);
body.data.forEach((sale) => rows.push(FIELDS.map((name) => (sale[name] === null ? "" : sale[name]))));
cursor = body.next_cursor;
if (!cursor) break;
}
sheet.clearContents();
sheet.getRange(1, 1, rows.length, FIELDS.length).setValues(rows);
}Apps Script fetches from Google’s servers, so the key never reaches the sheet’s viewers; but anyone who can edit the script can read its properties, so share editing only with people you would give the key to.
Keeping keys safe
- Keep keys where your code keeps secrets: an environment variable, a secret manager. Never in source control, a web page, an app others download, a shared document or a web address.
- Rotate by creating a second key, deploying it, then revoking the first: Pro holds two, so a rotation never needs downtime.
- Revoke a key the moment you think someone else has it, on your account page; it stops working on its next request. We email you whenever a key is created: if it was not you, revoke it and tell us.
- Each key shows when it was last used and how many sales it counted against the allowance this month (a sale counts to the key that fetched it first), so a key you thought idle is easy to spot.
The data, and the people in it
A sale carries what CSV export carries, including the borrower the notice names. The API finds sales by place and property, never by person: q matches addresses, cities, counties, parcel numbers (APN) and trustee sale numbers, and never a borrower.
Each record reflects a published notice of trustee’s sale or a trustee’s sale record as we last saw it, and is not an authoritative record of any sale, debt or title: sales are often postponed, reinstated or cancelled after publication. Confirm the date, time, place and terms with the trustee named in the record before you act. The data is not a consumer report, and the terms forbid using it to decide anyone’s credit, insurance, employment or housing, or to harass or defraud anyone named in it.
The terms, in short
Your keys are yours alone: use them from systems you control, within your plan’s limits, and keep them out of web pages and addresses. You may use the data in your own business; you may not resell or redistribute it in bulk, or use it to build a database or a service for others. The full text is in the terms, and what we store about keys and their use is in the privacy policy.
Questions
Can I use the API during the free trial?
No. Like CSV export, the data API opens with the first payment: during the 7-day free trial no key can be created. To use it the day you sign up, start your plan from your account page ("Start Pro now", under Subscription): the trial ends, the first payment is taken at checkout, and the API and CSV export open as soon as it goes through. The trial is then cancelled, so it is never charged as well.
Does fetching a sale again cost a record?
No. Through the API a sale counts once a calendar month (UTC), the first time any of your keys fetches it. Fetching it again that month is free, even once the allowance is spent, so a daily sync costs each sale once a month. Counts, coverage and your usage are always free.
Can I get CSV files through the API?
No. The API answers JSON only, and there is no format parameter. CSV export is part of the website: run a search while signed in and export it. It draws on the same monthly allowance, and it does not accept API keys.
Does the API include the text of the notice?
No. A sale through the API is the sale's record, as CSV export gives it: the property, the dates and status, the unpaid balance, the borrower, the beneficiary, the trustee and the rest of its fields. The original notice text stays on the sale's record page on the site, for plans that include it; each sale's url field links there.
Why can't I call the API from a web page?
Because a key in a web page is a published key: anyone who opens the page can read it and spend your allowance. The data endpoints send no CORS headers, so browsers refuse their answers. Call the API from a server, a script or a scheduled job, and give your own pages the results.
Is this how ChatGPT connects?
No. ChatGPT and other assistants connect under Connected apps on your account page: you sign in here, approve the app, and it gets tokens of its own, which the data API does not accept. The data API is for your own code, with keys you create.
Can I have the whole database?
Plans are for working your markets: the API's allowance is the one CSV export already has. For more than that, such as a full historical extract or a licensed feed into a product of your own, write to us through the contact form under "Bulk data, exports or an API".
Changelog
- Version 1, October 12, 2026: the first release. Sales (search, count, one sale), coverage (states and counties), the account, and the OpenAPI 3.1 description.