Developers
County Alerts API v1
Build resident alert sign-up on your county's own website. The County Alerts API lets your web server search properties, start a double opt-in sign-up, confirm it, email a manage link, unsubscribe, and read program totals, with the same safeguards as the hosted resident portal.
Overview and base URL
https://<your county>.sentradeed.com/api/county/v1
Requests and responses are JSON. Errors use the application/problem+json format with a human-readable title. Every call is scoped to the county that owns the API key; one county can never see another's data.
Authentication and scopes
Send an API key in the Authorization header:
Authorization: Bearer sdk_<prefix>_<secret>
- A county admin creates keys in the staff console under Settings → API keys. The full key is shown once; store it in your server's secret store.
- Each key belongs to one county. Keys can be revoked at any time and can be given an expiry date.
- Give each key only the scopes it needs:
| Scope | Allows |
|---|---|
parcels:read | Property search by address or APN. No owner data is ever returned. |
alerts:write | Sign-up, confirm, manage link and unsubscribe. |
alerts:read | Program totals (no personal data). |
Keys stay on your server
An API key is a server-side secret. Never put it in browser JavaScript or a mobile app. The API sends no CORS headers, so browsers cannot call it directly. Your page posts to your own server or serverless function, which adds the key and calls SentraDeed.
- Resident's browserYour sign-up page, with a CAPTCHA
- Your server or functionChecks the CAPTCHA, holds the API key
- SentraDeed County Alerts APIValidates, applies the rules, sends the confirmation email
- Resident's inboxClicks the link; only then do alerts start
Rules the API enforces
- Double opt-in. A sign-up always sends a confirmation email; nothing is watched until the resident clicks the link.
- Consent is required.
consentmust betrue, meaning the resident ticked your consent box. - A watch limit. One email can watch at most
max_watchesproperties and names in total (seeGET /config; 10 by default). - No owner data. The API never returns owner names or mailing addresses. Parcel numbers are masked.
- No account enumeration. Sign-up, manage-link and unsubscribe return the same response whether or not the email is known.
- Rate limits per key, and per email address for sign-up and manage-link requests.
- Bot protection is your job. Your page must protect the form with a CAPTCHA (for example Cloudflare Turnstile or hCaptcha) and check it on your server before calling the API.
- Where email links go. Confirmation and manage links point to the hosted resident portal by default. If the county's portal URL is set to your own page, the links go there instead, and your page calls
POST /alerts/confirmwith the link'stoken. - Every alert email includes a one-click unsubscribe link.
Endpoints
Examples use illustrative values. Common errors for every endpoint are listed under Errors and rate limits.
GET /config
County settings for your sign-up page. Any valid key.
{
"county": "Demo County",
"code": "demo",
"languages": ["en", "es"],
"max_watches": 10,
"portal_url": "https://demo.sentradeed.com",
"scopes": ["parcels:read", "alerts:write"]
}
GET /parcels/lookup?q=
Find a property by street address or APN (3–100 characters). Returns up to 5 active parcels. Scope: parcels:read.
GET /parcels/lookup?q=4417%20ELM
[
{
"id": "3f0c1a52-7d0e-4c1b-9a51-0d1c2e3f4a5b",
"situs": "4417 ELM GROVE WAY, DEMO CITY, 95822",
"apn_masked": "047-0213-•••-••••"
}
]
Errors: 422 if q is shorter than 3 characters.
POST /alerts/signup
Start a double opt-in sign-up. Sends the confirmation email; returns 202 with the same body for new and existing addresses. Scope: alerts:write.
{
"email": "[email protected]",
"parcel_ids": ["3f0c1a52-7d0e-4c1b-9a51-0d1c2e3f4a5b"],
"names": ["DOE JANE"],
"language": "en",
"consent": true
}
HTTP/1.1 202 Accepted
{ "message": "check_email" }
Errors: 422 with a title such as "Check the box to agree to receive alerts.", "Choose at least one property or add a name to watch.", "You can watch up to 10 properties and names." or "One of the properties could not be found. Search again." Also 429 for too many sign-ups for one email.
POST /alerts/confirm
Only for counties that host the confirmation page themselves: pass the token query parameter from the confirmation link. Confirmation links are valid for 48 hours. Scope: alerts:write.
{ "token": "eyJzaWQiOi…" }
HTTP/1.1 200 OK
{ "added": 2, "skipped": 0, "manage_token": "eyJzaWQiOi…" }
Errors: 400 "This link has expired or isn't valid. Request a new one."
POST /alerts/manage-link
Email the resident a link to manage their alerts. Same 202 response whether or not the address is subscribed. Scope: alerts:write.
{ "email": "[email protected]", "language": "es" }
HTTP/1.1 202 Accepted
{ "message": "check_email" }
POST /alerts/unsubscribe
Stop all alerts for an email address, for your own unsubscribe form. Same 202 response whether or not the address is subscribed. Scope: alerts:write.
{ "email": "[email protected]" }
HTTP/1.1 202 Accepted
{ "message": "unsubscribed" }
GET /alerts/stats
Program totals, with no personal data. Scope: alerts:read.
{
"active_subscribers": 128,
"active_watches": { "parcel": 171, "name": 22 },
"alerts_sent_last_30_days": 64,
"signups_last_30_days": 19
}
Errors and rate limits
| Status | When | Example title |
|---|---|---|
| 401 | Missing, invalid, revoked or expired key | "Send the API key as: Authorization: Bearer sdk_…", "Invalid API key.", "This API key has been revoked or has expired." |
| 403 | The key lacks the scope | "This API key doesn't have the alerts:write scope." |
| 400 | Expired or invalid confirmation token | "This link has expired or isn't valid. Request a new one." |
| 422 | A rule or field check failed | "Check the box to agree to receive alerts." (field-format errors use "Validation error" plus an errors list) |
| 429 | Rate limit reached; wait the number of seconds in the Retry-After header | "Rate limit reached." |
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{ "type": "about:blank", "title": "This API key doesn't have the alerts:write scope.", "status": 403 }
Rate limits in the current build: per key, 600 read calls and 120 write calls per minute; per email address, 3 sign-ups and 3 manage-link requests per hour. Limits may be tuned for production; always honor Retry-After.
Sign-up flow: curl, Node, Python
A minimal flow on your server: search for the property, then start the sign-up after your CAPTCHA check passes. Read the key from an environment variable; never hard-code it.
curl
BASE="https://demo.sentradeed.com/api/county/v1"
KEY="$SENTRADEED_API_KEY" # sdk_…, kept on the server
# 1. Find the property
curl -s "$BASE/parcels/lookup?q=4417%20ELM" \
-H "Authorization: Bearer $KEY"
# 2. Start the sign-up (after your CAPTCHA check)
curl -s -X POST "$BASE/alerts/signup" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","parcel_ids":["3f0c1a52-7d0e-4c1b-9a51-0d1c2e3f4a5b"],"language":"en","consent":true}'
Node.js (server-side fetch)
// Runs on your server or serverless function (Node 18+). Never ship the key to the browser.
const BASE = "https://demo.sentradeed.com/api/county/v1";
const headers = {
Authorization: `Bearer ${process.env.SENTRADEED_API_KEY}`,
"Content-Type": "application/json",
};
async function call(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers });
const body = await res.json();
if (res.status === 429) throw new Error(`Busy, retry in ${res.headers.get("Retry-After")} s`);
if (!res.ok) throw new Error(body.title); // show this message to the resident
return body;
}
export async function signUp({ email, query, consent, language = "en" }) {
// Verify your CAPTCHA token here first.
const parcels = await call(`/parcels/lookup?q=${encodeURIComponent(query)}`);
if (!parcels.length) throw new Error("No matching property.");
return call("/alerts/signup", {
method: "POST",
body: JSON.stringify({ email, parcel_ids: [parcels[0].id], language, consent }),
}); // { message: "check_email" }
}
Python (requests)
import os
import requests
BASE = "https://demo.sentradeed.com/api/county/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['SENTRADEED_API_KEY']}" # server-side only
def call(method, path, **kw):
r = S.request(method, BASE + path, timeout=15, **kw)
if r.status_code == 429:
raise RuntimeError(f"Busy, retry in {r.headers.get('Retry-After')} s")
if not r.ok:
raise ValueError(r.json().get("title")) # show this message to the resident
return r.json()
def sign_up(email, query, consent, language="en"):
# Verify your CAPTCHA token here first.
parcels = call("GET", "/parcels/lookup", params={"q": query})
if not parcels:
raise ValueError("No matching property.")
return call("POST", "/alerts/signup", json={
"email": email, "parcel_ids": [parcels[0]["id"]], "language": language, "consent": consent,
}) # {"message": "check_email"}
OpenAPI reference
The full, generated reference is published with the API:
- Full API reference (OpenAPI):
https://<your county>.sentradeed.com/api/county/v1/docs(interactive Redoc) - OpenAPI 3.1 JSON:
https://<your county>.sentradeed.com/api/county/v1/openapi.json. Use it with an OpenAPI generator to create a typed client in your language.
They are live at https://api.sentradeed.com/api/county/v1/docs and https://api.sentradeed.com/api/county/v1/openapi.json.
Versioning and support
- This is version 1. New optional fields and endpoints may be added to v1; breaking changes get a new version (
/api/county/v2), and counties are told before an old version is retired. - Changelog: v1 (September 2026), first release: config, parcel lookup, sign-up, confirm, manage link, unsubscribe, stats.
- Support: [email protected] or 559-251-7767. See also How counties send files, Security and Sub-processors.
See it run on your files
We'll walk your Recorder and Assessor teams through a full cycle on a few days of your own exports: intake, matching, exceptions, a print-ready batch and the resident portal.