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.
| Parameter | Type | Description |
|---|---|---|
| after_id | integer ≥ 0 | Return events with id strictly greater than this. Default 0 (start from the beginning). |
| limit | integer 1–1000 | Maximum 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.
| Parameter | Type | Description |
|---|---|---|
| from | YYYY-MM-DD | First UTC day, inclusive. Must be a real calendar date. |
| to | YYYY-MM-DD | Last 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=…"
}| Parameter | Type | Description |
|---|---|---|
| status | enum | pending · confirmed · unsubscribed · bounced |
| first_name | string | null | Null 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_name | string | null | Null unless the subscriber provided it on the post-confirmation profile page. |
| prefs.email.* | enum | Cadence per item: off · daily · weekly |
| age_range | enum | null | under_55 · 55_64 · 65_74 · 75_84 · 85_plus |
| gender | enum | null | woman · man · non_binary · self_described (free text then in gender_self) |
| categories | string[] | Any of: health, benefits, money, housing, caregiving, technology, travel, food |
| phone | string (absent by default) | Present only when the subscriber opted into SMS and provided a number. |
| confirm_url | url (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_url | url | Per-subscriber unsubscribe link. Must appear in every newsletter the platform sends. |
| profile_url | url (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, andsendNowunless the site is set to queue for 360REV's own schedule.newsletter.confirmedandnewsletter.profile_completed→POST /api/v1/sites/ingest/newsletter— an upsert carryingconsent: true. Everything without a field in that schema travels infields.newsletter.unsubscribedandnewsletter.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 areasonthat says which of the two it was.newsletter.signup(pending) → the same upsert withconfirm: trueandconfirmRedirectUrl— 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: trueis 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 carriesconsentorconfirm, never both. Every push carriesstatus, so an audience can be filtered to confirmed subscribers on the platform side.- Topic, frequency and demographic values travel in
fieldsand become filterable contact properties, so an audience can be segmented ontopics,frequency,ziporageRangewithout any change here. contact.message→POST /api/v1/sites/ingest/newsletter— the same upsert, carrying the visitor's note asfields.contactMessagewithfields.stream: "contact". It never assertsconsentand never sets a subscriberstatus, 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
- Raw analytics events never contain personal information; that rule is enforced when events are recorded.
- Contact records travel only inside
newsletter.*events, only over HTTPS, and only to the operator-configured endpoint. - Phone numbers are shared only for subscribers who opted into SMS.
- Subscriber action links (confirm, unsubscribe, profile) leave the site only as full HTTPS URLs inside contact records, only to the operator-configured endpoint — never as bare tokens, and never to any other party. They exist so the platform can send lifecycle email on the site's behalf.
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.