SeniorLiving.News

Friday, September 4, 2026
America's News for Seniors
Vol. 45 · Issue 15327
Sections & location
Share this paper

Newsletter API

Read and manage the subscriber list, and fetch the rendered newsletter, over authenticated REST.

The Newsletter API lets a connected platform work with the SeniorLiving.News mailing list directly: list subscribers, add and remove them, and fetch the issue that has been built for a given day. It is a pull API — you call us, on your own schedule, and you do not need to host any endpoint of your own.

It is independent of the Connector API, which pushes events out to a platform that implements a receiver. Either or both may be in use. This one needs only an API key.

Who sends email

SeniorLiving.News sends no email to a reader — not the newsletter, not the double opt-in confirmation, not a reply to a contact-us message. The connected platform executes every send, and this API is where the instructions for those sends travel:

ParameterTypeDescription
The newsletterissueGET /issues/latest returns the rendered edition to send.
Opt-in confirmationcontactSignups are pushed with confirm: true, so the platform mails its own signed confirmation link and records consent on the click. confirm_url here is the same signup's redirect target and a fallback link.
Contact-us repliesmessageGET /contact-messages returns what visitors wrote, with the address to reply to.
Unsubscribe receiptnoneNothing to send: the opt-out page confirms on screen, and the same event reaches the platform as a suppression.

Authentication

Every request carries an API key as a bearer token. Keys are issued from the site's admin area and shown exactly once at creation: only a SHA-256 fingerprint is stored, so a lost key cannot be recovered and must be replaced. A key can be revoked at any time, which takes effect immediately.

curl https://seniorliving.news/api/newsletter/v1/status \
  -H "Authorization: Bearer YOUR_API_KEY"

A missing, malformed, unknown, or revoked key returns 401. This differs from the Connector API's export endpoints, which answer 404 so as not to confirm that an endpoint exists. These endpoints are publicly documented, so there is nothing to conceal, and a clear 401 saves you from debugging a silent 404.

Conventions

All requests and responses are JSON. Timestamps are ISO-8601 in UTC. Email addresses are normalised to lowercase, so Pat@Example.com and pat@example.com are the same subscriber. An email in a URL path must be percent-encoded.

Errors

Errors carry a stable machine-readable code and a human message. Branch on the code, not the message — the wording may change.

{ "error": "consent_required",
  "message": "consent requires both `source` and `collected_at`, or neither." }
ParameterTypeDescription
unauthorized401Missing, unknown, or revoked API key.
invalid_request400The body or a query parameter failed validation. The message names the field.
consent_required400A consent claim was partial, unparseable, or dated in the future.
not_found404No subscriber or issue matched.
db_unavailable503Temporary — the request is safe to retry.

Pagination

List endpoints are cursor-paginated. Pass next_cursor from one response as ?cursor= on the next request; a null next_cursor means you have reached the end. Cursors are keyed on creation order rather than an offset, so walking the list never repeats or skips a subscriber even while signups are arriving. limit defaults to 100 and caps at 500.

The contact object

Every subscriber endpoint returns this shape. It is byte-identical to the contact payload the Connector API pushes, so the two integrations never disagree about what a subscriber looks like.

{
  "email": "pat@example.com",
  "first_name": "Pat",
  "last_name": "Rivera",
  "zip": "19103",
  "prefs": {
    "email": { "briefings": "daily", "puzzles": "daily",
               "onThisDay": "weekly", "local": "off" },
    "sms": { "enabled": false }
  },
  "status": "confirmed",
  "age_range": "65_74",
  "gender": null,
  "gender_self": null,
  "categories": ["health", "travel"],
  "profile_completed_at": "2026-08-03T15:12:09.000Z",
  "created_at": "2026-08-01T14:03:22.512Z",
  "confirmed_at": "2026-08-01T14:05:10.104Z",
  "unsubscribed_at": null,
  "unsubscribe_url": "https://seniorliving.news/unsubscribe?token=…",
  "profile_url": "https://seniorliving.news/subscribe/profile?token=…"
}
ParameterTypeDescription
statusenumpending · confirmed · unsubscribed · bounced. Only confirmed subscribers are ever sent the newsletter.
first_namestring | nullNull on most new contacts: signup asks only for email, ZIP and frequency. A name is collected later, optionally.
last_namestring | nullNull unless supplied on the post-confirmation profile page.
zipstring | null5 digits. Drives local news selection.
prefs.email.*enumPer-section cadence: off · daily · weekly.
categoriesstring[]Any of: health, benefits, money, housing, caregiving, technology, travel, food. Empty means all topics.
confirm_urlstringPresent only while confirmation is outstanding. The connector push already carries it as the double opt-in redirect target (confirm: true), so a platform-sent confirmation click both records consent and finalizes this record. It never expires or rotates: a re-signup keeps the same link.
unsubscribe_urlstringAlways present. Must appear in any email you send on our behalf.
phonestringPresent only when the subscriber has consented to SMS. Absent is not consent.

These URLs contain tokens. Anyone holding one can unsubscribe or edit that subscriber's profile, so treat a response from this API with the same care as the key itself. They are included because a platform sending on our behalf needs them.

Subscribers

GET https://seniorliving.news/api/newsletter/v1/status

Checks a key without touching the database. Use it to verify setup.

{ "ok": true, "version": "v1", "key": "360REV production" }

GET https://seniorliving.news/api/newsletter/v1/subscribers

ParameterTypeDescription
statusenum, optionalFilter to pending, confirmed, unsubscribed, or bounced. Omit for all.
cursorstring, optionalnext_cursor from the previous page.
limitint, optional1-500, default 100.
curl "https://seniorliving.news/api/newsletter/v1/subscribers?status=confirmed&limit=100" \
  -H "Authorization: Bearer YOUR_API_KEY"

{ "data": [ { … contact … }, … ], "next_cursor": "MjAyNi0wOC0wNVQx…" }

GET https://seniorliving.news/api/newsletter/v1/subscribers/{email}

Returns one contact, or 404.

POST https://seniorliving.news/api/newsletter/v1/subscribers

Adds a subscriber. Returns 201 for a new address and 200 for one already on the list — re-adding someone never erases profile data they have already given us.

curl -X POST https://seniorliving.news/api/newsletter/v1/subscribers \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "pat@example.com", "zip": "19103" }'
ParameterTypeDescription
emailstring, requiredNormalised to lowercase.
first_namestring, optional
last_namestring, optional
zipstring, optional5 digits, or omit.
phonestring, optionalStoring a number does not enable SMS; that needs separate consent.
prefsobject, optionalPartial. Anything omitted keeps its default.
categoriesstring[], optionalUnrecognised slugs are ignored rather than failing the request.
consentobject, optionalSee below. Required to create an already-confirmed subscriber.

Confirmed status and consent

By default a new subscriber is created as pending, and the response carries confirm_url. When the subscriber reaches that link — normally as the redirect target of the platform's own confirmation email, or sent directly — they become confirmed and start receiving the newsletter. This double opt-in is a legal requirement, not a preference, so there is no parameter that skips it.

If the subscriber already consented somewhere else — your own signup form, for instance — you may import them as confirmed by documenting where and when that consent was collected:

{ "email": "pat@example.com",
  "consent": { "source": "360REV signup form",
               "collected_at": "2026-07-14T10:02:00Z" } }

Both fields are required together. Supplying one without the other returns consent_required rather than quietly creating a pending subscriber, because a half-specified consent record looks documented and is not. A collected_at in the future, or one that cannot be parsed, is rejected for the same reason. The provenance is stored against the subscriber.

PATCH https://seniorliving.news/api/newsletter/v1/subscribers/{email}

Updates a subscriber. Omitting a field leaves it alone; sending null clears it. Those are different instructions, so send only what you intend to change.

# Change frequency to weekly, leaving everything else untouched
curl -X PATCH https://seniorliving.news/api/newsletter/v1/subscribers/pat%40example.com \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "prefs": { "email": { "briefings": "weekly" } } }'

# Unsubscribe
  -d '{ "status": "unsubscribed" }'

prefs is merged, not replaced: setting one cadence leaves the others as they were.

status accepts only "unsubscribed". A subscriber reaches confirmed by clicking their own confirmation link or through a documented consent record at creation — never by being set. Any other value returns invalid_request.

DELETE https://seniorliving.news/api/newsletter/v1/subscribers/{email}

Permanently erases the subscriber. This is not the same as unsubscribing, and the difference matters:

ParameterTypeDescription
PATCH status=unsubscribedsuppressionThe record stays, marked unsubscribed with a timestamp. They will not be emailed, and re-importing the address will not resurrect them silently. Use this when someone opts out.
DELETEerasureThe row is gone and cannot be recovered. Use this for a data-deletion request. Nothing then stops the same address being added again as a new subscriber.
{ "deleted": true, "email": "pat@example.com", "previous_status": "confirmed" }

Deleting also discards any queued outbound events carrying that address, so a pending sync cannot re-create the person after erasure.

Issues

An issue is recorded each time a digest runs. Both endpoints serve stored issues, so they answer immediately — nothing is rebuilt on demand.

GET https://seniorliving.news/api/newsletter/v1/issues/latest

The most recent issue, including its rendered HTML. This is what you fetch to send today's newsletter.

{
  "id": "0f1e…", "kind": "daily", "cadence": "daily",
  "date": "2026-08-05",
  "subject": "Wednesday, August 5, 2026: …",
  "site_url": "https://seniorliving.news",
  "postal_address": "…",
  "sections": {
    "briefing": { "date": "2026-08-05", "title": "…", "url": "…" },
    "on_this_day": [ { "year": 1969, "text": "…", "url": "…" } ],
    "puzzle_hint": "Today's puzzle: Sudoku",
    "topics": [ { "slug": "health", "label": "Health",
                  "summary": "…", "items": [ … ] } ]
  },
  "html": "<!doctype html>…"
}

The HTML contains the literal placeholder {{unsubscribe_url}}. Replace it per recipient with that contact's unsubscribe_url before sending. Mailing the placeholder unmodified sends bulk email with no working unsubscribe link, which is a legal problem, not a cosmetic one.

The issue carries no personalisation: no name, and no per-ZIP local news, because one body serves every recipient. Use sections if you would rather compose your own template — segmentation is yours to do, since you hold each contact's preferences.

404 means no issue has been built yet. One is recorded on every digest run.

GET https://seniorliving.news/api/newsletter/v1/issues

Issue history, newest first. The rendered HTML is omitted unless you pass ?include=html, since it is by far the largest field.

ParameterTypeDescription
limitint, optional1-500, default 100.
includestring, optionalSet to `html` to include each issue's rendered body.

When the site is sending its own email rather than handing off, an issue reflects the topics that run actually needed, which may be fewer than the full set.

Contact messages

What visitors write through the site's contact form. The site never replies by email itself — each message is also announced on the push side as a contact upsert carrying fields.contactMessage, but that upsert is fill-blanks-only, so a second message from the same address may not surface on the contact record. This endpoint is the lossless log: every message is its own row, in arrival order.

GET https://seniorliving.news/api/newsletter/v1/contact-messages

ParameterTypeDescription
cursorstring, optionalnext_cursor from the previous page.
limitint, optional1-500, default 100.
curl "https://seniorliving.news/api/newsletter/v1/contact-messages" \
  -H "Authorization: Bearer YOUR_API_KEY"

{ "data": [ {
    "id": "9b2e…",
    "email": "visitor@example.com",
    "name": "Grace Hopper",
    "message": "How do I submit a community event?",
    "submitted_at": "2026-08-12T10:30:00.000Z"
  } ],
  "next_cursor": null }

name is null when the visitor left it blank. Writing a message is not a newsletter opt-in: contact-form senders are never asserted as consented, and they carry no subscriber status, so an audience filtered on either can't include them.

Rate limits and retries

There is no request quota. Please stay considerate: poll /issues/latest once per send rather than continuously, and walk /subscribers with the cursor instead of re-reading the list. A 503 is temporary and safe to retry with backoff; a 4xx will not succeed on retry without a change to the request.

Data & privacy

This API exposes personal data belonging to real subscribers, along with tokens that act on their behalf. Every write is recorded with the action, the address, and the outcome; deletion records are retained deliberately, so that an erasure can be evidenced. If you send email using this data you inherit the obligations that come with it — honouring unsubscribes promptly, including the postal address in the footer, and never mailing a subscriber whose status is anything other than confirmed.