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

  1. 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.
  2. Create a key under Data API on your account page, and copy it when it is shown: it is shown once.
  3. Keep it in an environment variable on the machine that calls the API, here TRUSTEESALEDATA_API_KEY, and send it in the Authorization header:
Your account, plan and usage
curl https://www.trusteesaledata.com/api/v1/me \
  -H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"
Upcoming sales in Arizona, soonest first
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 an X-API-Key header, 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’s request_id too. 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 is null, never left out.
  • No CSV. The API has no CSV format and no format parameter. 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 an Authorization header 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/v1 need no key.

Plans and limits

The data API on each plan
LimitStarterProBusiness
Price$29 a month$49 a month$99 a month
The data API—IncludedIncluded
Records a monthBy CSV export and the API together. Through the API, a sale counts once a month.—3,00010,000
New records through the APIIn any 24 hours—1,0002,500
Requests a minuteEvery key together—60120
Requests a dayUTC, from midnight—5,00010,000
Requests at once—23
Live keys—25
Sales per page100 unless asked—Up to 200Up 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/v1 and openapi.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: true and a next_cursor that resumes at the first one held back. Only when not one sale on a page can be served is the answer 429 quota_exceeded, whose details.limit says which allowance (month or day) and whose Retry-After says 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, day or in_flight). Wait Retry-After seconds 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 /me and 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.

Response headers
HeaderWhat it says
X-Request-IdThis response's id; quote it to support. The same as error.request_id.
RateLimit-LimitRequests 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-RemainingRequests left in the sliding minute.
RateLimit-ResetSeconds until the oldest request in the minute leaves it (0 when none is counted).
RateLimit-PolicyEvery request window: requests a minute, then a UTC day (60;w=60, 5000;w=86400).
X-Quota-Month-LimitRecords a month, by CSV export and the API together.
X-Quota-Month-UsedRecords used this month: CSV rows plus distinct sales through the API.
X-Quota-Month-RemainingRecords left this month.
X-Quota-Month-ResetWhen the month starts again: 00:00 UTC on the 1st.
X-Quota-Day-LimitNew records the API may count in any 24 hours.
X-Quota-Day-RemainingNew records left in the last 24 hours.
X-Records-CountedSales this response counted: those not already fetched this month.
Retry-AfterOn 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.

ParameterAcceptsWhat it does
qText, 1 to 100 charactersText to find in a sale's address, city, county, parcel number (APN) or trustee sale number. Never people: borrowers are not searched.
stateA two-letter codeTwo-letter state code: one /states lists, or any state a sale is in.
countyText, 1 to 80 charactersA county, spelled exactly as /states/{state}/counties or a sale's county gives it. Needs state.
cityText, 1 to 60 charactersA city in the state, in any case: Phoenix or phoenix. Needs state.
statusOne of upcoming, past, all, postponed; upcoming when left outupcoming (from today), past, all, or postponed (sales the trustee has postponed).
sale_date_fromA date, YYYY-MM-DDSales on or after this date (YYYY-MM-DD), after any postponement. The search page calls it from.
sale_date_toA date, YYYY-MM-DDSales on or before this date (YYYY-MM-DD), after any postponement. The search page calls it to.
first_seen_afterAn ISO 8601 date and time with its offset, or a dateSales 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_balanceA number of US dollars, 0 or moreUnpaid balance at least this many US dollars. The search page calls it minBalance.
max_unpaid_balanceA number of US dollars, 0 or moreUnpaid balance at most this many US dollars. The search page calls it maxBalance.
sortOne of sale_date, -sale_date, first_seen, -first_seen, unpaid_balance, -unpaid_balancesale_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.
limitA whole number, 1 to 200; 100 when left outSales per page, 1 to 200.
cursorAs it came, up to 512 charactersThe next_cursor of the previous page, as it came. Valid only with the same filters and sort.
Request
curl "https://www.trusteesaledata.com/api/v1/sales?state=AZ&status=upcoming&limit=100" \
  -H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"
Response
{
  "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.

ParameterAcceptsWhat it does
stateA two-letter codeTwo-letter state code: one /states lists, or any state a sale is in.
countyText, 1 to 80 charactersA county, spelled exactly as /states/{state}/counties or a sale's county gives it. Needs state.
cityText, 1 to 60 charactersA city in the state, in any case: Phoenix or phoenix. Needs state.
statusOne of upcoming, past, all, postponed; upcoming when left outupcoming (from today), past, all, or postponed (sales the trustee has postponed).
sale_date_fromA date, YYYY-MM-DDSales on or after this date (YYYY-MM-DD), after any postponement. The search page calls it from.
sale_date_toA date, YYYY-MM-DDSales on or before this date (YYYY-MM-DD), after any postponement. The search page calls it to.
first_seen_afterAn ISO 8601 date and time with its offset, or a dateSales 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_balanceA number of US dollars, 0 or moreUnpaid balance at least this many US dollars. The search page calls it minBalance.
max_unpaid_balanceA number of US dollars, 0 or moreUnpaid balance at most this many US dollars. The search page calls it maxBalance.
Response
{
  "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.

Response, asked by an earlier id
{
  "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.

A state
FieldTypeWhat it holds
statestringTwo-letter state code.
namestringThe state's name.
upcoming_salesintegerSales from today on.
next_30_daysintegerSales in the next 30 days.
held_last_90_daysintegerSales held in the last 90 days.
next_sale_datestring (date), or nullThe next sale's date.
countiesintegerCounties with sales: the ones /states/{state}/counties lists.
urlstring (uri)The state's page on the site.
Response
{
  "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.

A county
FieldTypeWhat it holds
countystringThe county, spelled exactly as the county filter wants it.
statestringTwo-letter state code.
upcoming_salesintegerSales from today on.
next_30_daysintegerSales in the next 30 days.
held_last_90_daysintegerSales held in the last 90 days.
next_sale_datestring (date), or nullThe next sale's date.
urlstring (uri), or nullThe county's page on the site; null for a county that has none.
Response
{
  "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.

Response
{
  "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.

A sale
FieldTypeWhat it holds
idstringThe 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.
addressstring, or nullThe property's address as published.
citystring, or nullThe property's city.
countystring, or nullThe county, spelled as the county filter wants it.
statestringTwo-letter state code.
zipstring, or nullThe property's ZIP code.
apnstring, or nullThe parcel number (APN) as published.
sale_datestring (date), or nullThe date the sale is set for, after any postponement.
sale_timestring, or nullThe time of the sale as published.
sale_statusstring, or nullThe sale's status: Scheduled, Postponed or Sold, where one is given.
postponed_tostring (date), or nullThe date a postponement moved the sale to.
unpaid_balancenumber, or nullThe unpaid balance the notice states, in US dollars.
opening_bidnumber, or nullThe opening bid in US dollars, where one is stated.
original_loan_amountnumber, or nullThe loan's original principal as the notice states it, in US dollars. Not the balance.
borrowerstring, or nullThe borrower or owner the notice names.
beneficiarystring, or nullWho the debt is owed to: the lender or its successor.
trusteestring, or nullThe trustee conducting the sale.
sale_linestring, or nullThe trustee's sale information line.
trustee_sale_numberstring, or nullThe trustee's sale number.
sale_locationstring, or nullWhere the sale is held.
first_seen_atstring (date-time)When we first saw the sale: the order for incremental sync (sort=first_seen).
last_seen_atstring (date-time)When it was last seen in a published notice or a trustee's sale record.
urlstring (uri)The sale's record page on the site.
other_idsarray of stringsThe ids of the sale's other notices and sale records, newest first. An id you hold that is here is this sale.
property_typestring, or nullThe kind of property, where it is stated.
other_noticed_datesarray of stringsThe sale's other noticed dates, when one loan was noticed for more than one (YYYY-MM-DD, ascending).
sale_date_unverifiedbooleanThe 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

  1. Once: walk your markets’ upcoming sales (the default status) in the order we first saw them, to the end, keeping each sale by its id: /sales?state=AZ&sort=first_seen.
  2. Every day, or as often as every hour (new sales are added through the day): ask for what is new since, first_seen_after set a few minutes before the newest first_seen_at you hold. A sale you already have comes back free.
  3. 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.

400, an impossible date
{
  "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"
  }
}
Error codes
CodeStatusWhenWhat to do
invalid_parameter400A 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_parameter400A 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_cursor400The 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_url400An 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.
unauthorized401No Authorization: Bearer header was sent. A website sign-in cookie does not authenticate the API.Send Authorization: Bearer <key>.
invalid_key401The 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_plan403The 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_limit403The 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_due403The 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_inactive403The account's plan is not active, so the API is closed.Contact us if this is unexpected.
not_found404No 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_allowed405The data API is read-only and answers GET only.Use GET.
gone410The id belonged to a sale that is no longer listed: withdrawn, cancelled or otherwise off the list.Remove it from your copy.
rate_limited429Too 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_exceeded429The 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_error500Something went wrong on our side.Retry later; quote the request_id if it keeps happening.
busy503The 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_disabled503The 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
curl "https://www.trusteesaledata.com/api/v1/sales?state=AZ&status=upcoming&limit=100" \
  -H "Authorization: Bearer $TRUSTEESALEDATA_API_KEY"
Python (requests)
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"])
JavaScript (Node)
// 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 (Apps Script)
// 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.