Senior Living Canonical API
One stable REST contract. Any vendor on the other side. No data or credentials stored on our platform.
How it works — in one request
GET /v1/communities/<communityId>/residents
Authorization: Bearer osl_prd_<platform-api-key>
X-Source: speak2_family ← which vendor
X-Source-Authorization: <base64 of vendor credentials JSON> ← we never store this
Customers send a platform API key (identifies the caller) plus per-request vendor credentials. The platform translates live vendor calls into a canonical shape and returns them. No storage, no sync, no shared-secret liability.
Try a real request in the Playground →
Connectors
| Source | Status | What it exposes | Credentials JSON |
speak2_family |
Live |
communities, residents, events, menu days |
{ apiKey, username, password } |
go_icon |
Live |
residents, events, contacts (staff) |
{ clientId, clientSecret } or { accessToken } |
worxhub |
Live |
tickets (list, get, create) |
{ host, instance, clientId, clientSecret } or { host, instance, accessToken } |
tels |
Stub |
— |
Pending doc review |
point_click_care |
Future |
— |
TBD |
Endpoints
Key validation (no vendor call)
| Method | Path | Notes |
| GET | /v1/_me | Echoes the caller's tenant + key metadata. Useful for CI smoke tests and SDK "is this key live?" checks. Requires the platform API key only — no X-Source headers needed. |
Communities / discovery
| Method | Path | Supported by |
| GET | /v1/communities | speak2_family |
Residents
| Method | Path | Supported by |
| GET | /v1/communities/{communityId}/residents | speak2_family, go_icon |
| GET | /v1/communities/{communityId}/residents/{residentId} | go_icon |
Events
| Method | Path | Supported by |
| GET | /v1/communities/{communityId}/events | speak2_family, go_icon |
Menu days
| Method | Path | Supported by |
| GET | /v1/communities/{communityId}/menu-days | speak2_family |
| GET | /v1/communities/{communityId}/menu-days/{YYYY-MM-DD} | speak2_family |
Contacts
| Method | Path | Supported by |
| GET | /v1/communities/{communityId}/contacts | go_icon |
Tickets
| Method | Path | Supported by |
| GET | /v1/communities/{communityId}/tickets | worxhub (requires ?resident_id=) |
| GET | /v1/communities/{communityId}/tickets/{ticketId} | worxhub |
| POST | /v1/communities/{communityId}/tickets | worxhub |
| PATCH | /v1/communities/{communityId}/tickets/{ticketId} | — (WorxHub doesn't expose update) |
Writes require an API key with scope write:tickets. Pass Idempotency-Key: <your-id> on POST — we cache the canonical response for 24h so retries don't duplicate in the source system.
Canonical response envelope
Every canonical record includes these fields at minimum:
{
"id": "vendor-native-id",
"source": "speak2_family" | "go_icon" | "worxhub" | ...,
"createdAt": "optional, vendor-reported ISO 8601",
"updatedAt": "optional, vendor-reported ISO 8601",
...canonical fields for the specific resource...
}
Errors
Clients should branch on error.code (stable). error.message is human-readable and can change.
{
"error": {
"code": "unauthorized",
"message": "Vendor auth rejected (HTTP 401).",
"requestId": "req_abcdef1234567890",
"details": { "detail": "..." }
}
}
| code | HTTP | Typical cause |
| bad_request | 400 | Malformed input, invalid dates, unsupported source header. |
| unauthorized | 401 | Platform API key invalid or vendor credentials rejected at source. |
| forbidden | 403 | API key missing required scope. |
| resource_not_found | 404 | Record or endpoint not found. |
| unprocessable | 422 | Source doesn't support this operation, or malformed body. |
| rate_limited | 429 | Vendor-side rate limit exceeded. Retry with backoff. |
| upstream_unavailable | 502 | Vendor timed out or returned 5xx. Already retried 3×. |
Customer onboarding (operator steps)
Each customer is a tenant. Tenants own zero-or-more API keys. Every key carries a tenantId, so every request traces back to its customer.
# 1. Create the tenant
POST /admin/organizations
{ "name": "Joy Living", "primaryContactEmail": "ops@joyliving.com" }
→ { "id": "org_…", "status": "active", … }
# 2. Issue the customer's platform API key
POST /admin/api-keys
{
"tenantId": "org_…",
"label": "Joy prod — ops backend",
"environment": "prod",
"scopes": ["read", "write:tickets"]
}
→ { "key": {…meta, no plaintext…}, "plaintext": "osl_prd_Fg3aH7q2X9mK…" }
# — plaintext is returned ONCE. Hand off to the customer, never shown again.
# 3. Customer validates the key landed correctly
GET /v1/_me
Authorization: Bearer osl_prd_…
→ { "apiKey": { "id": "key_…", "scopes": [...], "environment": "prod", … },
"tenant": { "id": "org_…", "name": "Joy Living", "status": "active" } }
Every request is logged
The requestAudit middleware records one entry per request in auditLogs/*, indexed by actorId (= key id). Entries include method, path, status, durationMs, source (when sent), external scope id, and request id. Never bodies, never credentials. Query via GET /admin/audit with filters for tenantId, apiKeyId, action, or source.
curl "$API/admin/audit?apiKeyId=key_…&limit=100" \
-H "Authorization: Bearer $ADMIN_KEY"
Security posture
- Platform API keys are stored as SHA-256 hashes — plaintext shown once at issuance.
- Vendor credentials flow on every request and are never persisted, logged, or cached on disk.
- Per-process in-memory token caches (Go Icon and TheWorxHub OAuth) die with the Cloud Functions instance — no cross-request leak on restart.
- Ticket-create idempotency is the only hashed side-state: keyed by
sha256(apiKeyId + idempotencyKey), 24h TTL, canonical response body only.
- Audit log records request metadata only — never request or response bodies, never credentials.
Built with pass-through architecture: ONSCREEN never stores your residents, events, or work orders. Your vendor is the system of record; we just translate the shape.