SeniorLiving.News

Thursday, September 17, 2026
America's News for Seniors
Vol. 58 · Issue 36905
Sections & location
Share this paper

Connector API

Integration reference for connecting an analytics or CRM platform to SeniorLiving.News.

SeniorLiving.News can exchange data with one connected platform through two channels: export endpoints hosted on this site that the platform pulls from, and an ingest contract the platform implements so the site can push events to it in near real time. Both channels are disabled unless the site operator has configured a connection; there is no public or anonymous access to any of the data described here.

Authentication

Every request in both directions carries the same shared secret, arranged with the site operator, as a bearer token:

Authorization: Bearer <token>

Requests to the export endpoints with a missing or invalid token — or while no connection is configured — receive 404 Not Found, deliberately indistinguishable from the endpoint not existing.

Export endpoints (hosted by SeniorLiving.News)

GET https://seniorliving.news/api/export/status

Connection test. Verifies the bearer token against an active connection and nothing else — it reads no data and has no side effects, so it is safe to poll. Use this as the “test path” when configuring a platform's connection form.

{ "ok": true }

GET https://seniorliving.news/api/export/events

Raw first-party analytics events (pageviews, link clicks, engagement pings), paginated by an exact integer cursor. Events contain no personally identifying information — that is enforced when events are captured, not filtered at export time.

ParameterTypeDescription
after_idinteger ≥ 0Return events with id strictly greater than this. Default 0 (start from the beginning).
limitinteger 1–1000Maximum events per page. Default 500.
{
  "events": [
    {
      "id": 4182,
      "occurred_at": "2026-08-03T14:07:11.402Z",
      "event_type": "pageview",
      "visitor_id": "9f2c4a1e77b04d1c",
      "session_id": "b81d02c6a4e94f02",
      "path": "/briefing",
      "referrer": "https://news.google.com/",
      "props": null
    }
  ],
  "next_after_id": 4182
}

Pass next_after_id back as after_id to fetch the next page. When events comes back empty, next_after_id equals the cursor you sent — you are caught up. Ids are gap-free per row but not guaranteed contiguous; treat the cursor as opaque. A malformed query returns 400.

GET https://seniorliving.news/api/export/rollups

Daily metric rollups, computed on demand for a UTC date range. Nothing is precomputed or cached, so prefer modest ranges.

ParameterTypeDescription
fromYYYY-MM-DDFirst UTC day, inclusive. Must be a real calendar date.
toYYYY-MM-DDLast UTC day, inclusive. Must be ≥ from; the whole span may cover at most 92 days.
{
  "rollups": [
    {
      "date": "2026-08-03",
      "source": "seniorliving.news",
      "pageviews": 412,
      "unique_visitors": 63,
      "new_visitors": 9,
      "sessions": 120,
      "issue_number": 84,
      "top_pages": [{ "path": "/briefing", "views": 88 }],
      "link_clicks": 57,
      "avg_engagement_seconds": 74,
      "llm_usage": {
        "calls": 6,
        "failures": 1,
        "by_model": [
          {
            "model": "360rev:claude-opus-4-8",
            "calls": 5,
            "failures": 1,
            "prompt_tokens": 60321,
            "completion_tokens": 24110
          }
        ]
      },
      "ga4": null
    }
  ]
}

top_pages lists at most ten paths by pageviews. avg_engagement_seconds is null on days with no engagement events. ga4 is reserved for Google Analytics figures and is currently always null. A quiet day returns zeros — the absence of a rollup, not zeros, signals a problem. A malformed or oversized range returns 400.

Ingest contract (implemented by the connected platform)

The connected platform exposes two endpoints under a base URL of its choosing. SeniorLiving.News calls them with the same bearer token.

POST {base}/v1/events

The site pushes events in JSON batches of up to 100. Each event carries a UUID id that serves as an idempotency key — retries can redeliver a batch, so the receiver must deduplicate on it.

[
  {
    "id": "5f0f9a3e-1c2b-4a6d-9e8f-0b1c2d3e4f5a",
    "type": "newsletter.signup",
    "occurred_at": "2026-08-03T14:03:22.512Z",
    "payload": { … }
  }
]

Any 2xx response means the entire batch is durably accepted. Any other response — or a network failure, or exceeding the 15-second request timeout — causes the site to retry the batch with backoff: after 1 minute, 10 minutes, 1 hour, 6 hours, then every 24 hours, giving up after 20 attempts. Delivery is near-real-time when the receiver is healthy and self-heals after an outage.

Newsletter events

newsletter.signup, newsletter.confirmed, newsletter.unsubscribed, and newsletter.bounced each carry the subscriber's contact record as the payload:

{
  "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
first_namestring | nullNull until the subscriber fills it in after confirming — the signup form asks only for email, ZIP and frequency, so expect null on most new contacts.
last_namestring | nullNull unless the subscriber provided it on the post-confirmation profile page.
prefs.email.*enumCadence per item: off · daily · weekly
age_rangeenum | nullunder_55 · 55_64 · 65_74 · 75_84 · 85_plus
genderenum | nullwoman · man · non_binary · self_described (free text then in gender_self)
categoriesstring[]Any of: health, benefits, money, housing, caregiving, technology, travel, food
phonestring (absent by default)Present only when the subscriber opted into SMS and provided a number.
confirm_urlurl (pending only)Double-opt-in finalizer link. Present only while status is pending. On the 360REV wire it becomes confirmRedirectUrl: the platform's own confirmation email lands the reader here after their click is verified.
unsubscribe_urlurlPer-subscriber unsubscribe link. Must appear in every newsletter the platform sends.
profile_urlurl (optional)Link to the subscriber's preference/profile page, when one exists.

Platform responsibilities: a newsletter.signup event with status: "pending" arrives on the wire with confirm: true, so the platform sends its own signed confirmation email and records consent when the reader clicks. The click then lands on confirm_url (sent as confirmRedirectUrl), which marks the subscriber confirmed here and pushes a newsletter.confirmed event back. Include unsubscribe_url in every newsletter sent to that subscriber.

Newsletter issues

When the site's delivery is in connector mode, each digest run pushes one newsletter.issue event — the fully generated newsletter for the platform to send to the list it maintains from the contact events above. Segmentation (daily vs. weekly cadence, category interests) is the platform's job, using the prefs on each contact record.

Live integration — 360REV. The structure above is what the site builds internally. On the wire it is mapped to 360REV's ingest contract, one object per request:

  • newsletter.issue POST /api/v1/sites/ingest/newsletter-issue subject, html, idempotencyKey, and sendNow unless the site is set to queue for 360REV's own schedule.
  • newsletter.confirmed and newsletter.profile_completed POST /api/v1/sites/ingest/newsletter — an upsert carrying consent: true. Everything without a field in that schema travels in fields.
  • newsletter.unsubscribed and newsletter.bounced POST /api/v1/sites/ingest/unsubscribe — the address goes on 360REV's suppression list, which is enforced on every dispatch platform-wide. Both carry a reason that says which of the two it was.
  • newsletter.signup (pending) → the same upsert with confirm: true and confirmRedirectUrl — 360REV runs the double opt-in itself: it mails its own signed link, records consent on the click, and redirects the reader to this site's confirm URL so the local record flips too.
  • consent: true is asserted only for a subscriber who completed double opt-in. A pending signup is still pushed — the key is simply absent, which records nothing rather than claiming something untrue. A body carries consent or confirm, never both. Every push carries status, so an audience can be filtered to confirmed subscribers on the platform side.
  • Topic, frequency and demographic values travel in fields and become filterable contact properties, so an audience can be segmented on topics, frequency, zip or ageRange without any change here.
  • contact.message POST /api/v1/sites/ingest/newsletter — the same upsert, carrying the visitor's note as fields.contactMessage with fields.stream: "contact". It never asserts consent and never sets a subscriber status, so an audience filtered on either cannot pick these up.
{
  "kind": "daily",           // daily · weekly · manual · test
  "cadence": "daily",
  "date": "2026-08-04",
  "subject": "Tuesday, August 4, 2026: Medicare premiums hold steady",
  "site_url": "https://seniorliving.news",
  "postal_address": "… or null",
  "sections": {
    "briefing": { "date": "2026-08-04", "title": "…", "url": "https://seniorliving.news/briefing/2026-08-04" },
    "on_this_day": [ { "year": 1965, "text": "…" } ],
    "puzzle_hint": "Today's puzzle: Trivia",
    "topics": [
      { "slug": "health", "label": "Health & Wellness", "summary": "…",
        "items": [ { "title": "…", "url": "…", "source": "…" } ] }
    ]
  },
  "html": "<!doctype html>…"
}

html is a ready-to-send rendering of the standard issue template containing the literal placeholder {{unsubscribe_url}} — the platform MUST substitute each recipient's own unsubscribe link (from their contact record) before sending, or use its own compliant footer. sections carries the same content as structured data for platforms that compose their own templates. Treat kind values test and manual as operator-initiated runs; only daily and weekly are scheduled issues.

Contact-us messages

contact.message carries what a visitor wrote through the site's contact form. The site never replies by email itself — the platform holds the sender's address, their name when given, and the message, and the reply is the platform's to send. Messages queue durably, so ones written before the connector was activated arrive with the first sync after it. The authoritative per-message log is also readable from the Newsletter API's /contact-messages endpoint.

{
  "email": "visitor@example.com",
  "name": "Grace Hopper",
  "message": "How do I submit a community event?",
  "submitted_at": "2026-08-12T10:30:00.000Z"
}

Analytics events

analytics.daily_rollup carries one UTC day's metrics — the payload shape is identical to a single entry in the export rollups endpoint above. One rollup is pushed per completed UTC day.

Data & privacy

Questions about integrating, or need credentials? Contact the site operator. This page documents the contract as currently deployed; breaking changes will be versioned under a new path prefix.