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:
API key scopes
ScopeAllows
parcels:readProperty search by address or APN. No owner data is ever returned.
alerts:writeSign-up, confirm, manage link and unsubscribe.
alerts:readProgram 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.

  1. Resident's browserYour sign-up page, with a CAPTCHA
  2. Your server or functionChecks the CAPTCHA, holds the API key
  3. SentraDeed County Alerts APIValidates, applies the rules, sends the confirmation email
  4. Resident's inboxClicks the link; only then do alerts start
Recommended architecture: browser → your server (API key) → SentraDeed → confirmation email to the resident.

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. consent must be true, meaning the resident ticked your consent box.
  • A watch limit. One email can watch at most max_watches properties and names in total (see GET /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/confirm with the link's token.
  • 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

Error responses
StatusWhenExample title
401Missing, 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."
403The key lacks the scope"This API key doesn't have the alerts:write scope."
400Expired or invalid confirmation token"This link has expired or isn't valid. Request a new one."
422A rule or field check failed"Check the box to agree to receive alerts." (field-format errors use "Validation error" plus an errors list)
429Rate 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.

Request a demo