Creator Notify v1.0.0

Lifecycle & expiration

Two independent clocks exist in Creator Notify, and confusing them causes most support questions: the registration expires when you stop publishing, and individual subscriptions expire when the browser or push service drops them.

The registration clock

Every registered website carries expires_at. It is set at registration to created_at + INACTIVITY_DAYS (7 by default) and moved forward on every successful publish.

EventResets the clock?Why
POST /v1/notifications accepted (2xx)YesThis is the only activity that counts.
Publish where every push submission failedYesThe request itself was valid and authorised; per-subscription outcomes are a delivery concern, reported separately.
Publish with zero subscribersYesAuthorised, well-formed activity. Response warns NO_ACTIVE_SUBSCRIPTIONS.
Subscription created, refreshed or removedNoSubscriber traffic is not a publish.
GET /health, GET /v1/sites/statusNoReads never count.
Failed publish (400/403/413/429/5xx)NoA rejected request is not a trigger.
POST /v1/sites/reactivateYesExplicitly restarts the window from now.

Policy chosen at registration

The window starts at the moment of registration (created_at), not at the first publish. A brand-new registration therefore has exactly seven days to send its first notification.

How expiry is enforced

  1. At request time. Every authenticated request compares expires_at with the current UTC time. If the window has elapsed the registration is marked expired in the same request and the API returns 403 SITE_EXPIRED. This is the authoritative check — it cannot be skipped, scheduled task or not.
  2. By the Cron Trigger. Every 15 minutes the Worker sweeps UPDATE sites SET status='expired' WHERE status='active' AND expires_at <= now, so status endpoints, dashboards and operators see the correct state promptly without any request arriving.

All timestamps are ISO-8601 UTC strings, which sort chronologically as text, so the comparison is both correct and indexable.

curl -s https://YOUR-API-HOST/v1/sites/status -H "Authorization: Bearer $KEY"
# ...
# "status": "expired",
# "expires_at": "2026-02-07T12:00:00.000Z",
# "expires_in_seconds": 0

What an expired registration can and cannot do

OperationExpired registration
POST /v1/notificationsBlocked — 403 SITE_EXPIRED
POST /v1/subscriptionsBlocked — new subscribers are not collected
GET /v1/sites/status, lists, countsAllowed (read-only inspection)
POST /v1/sites/reactivateAllowed — this is the recovery path
POST /v1/sites/rotate-keyAllowed

Existing subscriptions are not deleted, and notification history is not deleted. Lifecycle and retention are separate concerns: expiry changes a status flag, retention deletes old rows after NOTIFICATION_RETENTION_DAYS regardless of status.

The error

{
  "success": false,
  "error": {
    "code": "SITE_EXPIRED",
    "message": "This registration expired on 2026-02-07T12:00:00.000Z because no notification was published for 7 day(s). POST /v1/sites/reactivate to resume.",
    "details": {
      "expired_at": "2026-02-07T12:00:00.000Z",
      "last_triggered_at": "2026-01-31T12:00:00.000Z",
      "inactivity_days": 7,
      "reactivate_endpoint": "/v1/sites/reactivate"
    }
  },
  "request_id": "req_9x2..."
}

Reactivation

curl -X POST https://YOUR-API-HOST/v1/sites/reactivate \
  -H "Authorization: Bearer $CREATOR_NOTIFY_KEY"
{
  "success": true,
  "site": { "status": "active", "expires_at": "2026-02-14T12:30:00.000Z", "...": "..." },
  "reactivated": true,
  "expires_at": "2026-02-14T12:30:00.000Z",
  "note": "Subscriptions were not touched. Notifications can be published again immediately."
}
  • The API key is still required — reactivation is not a way to take over someone else's registration.
  • The new window starts now, not at the old expires_at, so reactivation cannot be used to bank unused days.
  • last_reactivated_at records the event for auditing.
  • A suspended registration cannot be reactivated; see below.

If the key is lost

Re-register the same origin with POST /v1/sites/register. The old row still owns the origin, so the request fails with DUPLICATE_SITE; an operator then either deletes the stale row (DELETE FROM sites WHERE id = ?, which cascades) or restores access. This is intentionally not self-service.

Key rotation

curl -X POST https://YOUR-API-HOST/v1/sites/rotate-key \
  -H "Authorization: Bearer $OLD_KEY"
  • The response contains the new key exactly once.
  • key_version increments; key_rotated_at is recorded.
  • The old key fails immediately with INVALID_API_KEY. There is no grace period.
  • Rotation does not touch subscriptions, notifications or the inactivity window.

Recommended cadence: rotate on staff changes, on suspected exposure, and at least once a year.

Revocation

curl -X POST https://YOUR-API-HOST/v1/sites/revoke-key \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"confirm":true}'

Clears the stored hash and sets status = 'suspended'. The registration can no longer publish, collect subscriptions, rotate or reactivate. Data is retained. Recovery is an operator action (re-register the origin, or ask the operator to lift the suspension).

Subscription lifecycle (a different clock)

Subscription statusSet whenEffect
activeCreated, refreshed, or re-subscribedIncluded in publishes.
invalidPush service returned 404 or 410, or the stored auth secret failed to decryptExcluded from future publishes. The operator sees the count in the delivery report.
unsubscribedDELETE /v1/subscriptionsExcluded. Purged after UNSUBSCRIBED_RETENTION_DAYS.

Browsers invalidate subscriptions constantly: cache clears, permission revocation, long inactivity by the vendor, OS migration, private browsing. An invalid rate of a few percent per send is normal; a sudden jump usually means a bug in the Service Worker path or a payload the platform rejects.

Retention

DataVariableDefault
Notifications (and their delivery attempts, by cascade)NOTIFICATION_RETENTION_DAYS90 days
Delivery attemptsDELIVERY_ATTEMPT_RETENTION_DAYS30 days
Unsubscribed subscriptionsUNSUBSCRIBED_RETENTION_DAYS30 days
Rate-limit countersRATE_LIMIT_RETENTION_HOURS24 hours
Registrations, active/invalid subscriptions—Kept until an operator deletes them

Retention runs inside the same Cron Trigger, in the same invocation as the expiry sweep.

Example timeline

2026-01-01 09:00 UTC  register        status=active   expires_at=2026-01-08 09:00
2026-01-03 10:00 UTC  publish         status=active   expires_at=2026-01-10 10:00  last_triggered_at set
2026-01-04 11:00 UTC  subscription    no change       expires_at=2026-01-10 10:00
2026-01-05 12:00 UTC  publish (400)   no change       expires_at=2026-01-10 10:00
2026-01-10 10:00 UTC  window ends     status=expired  (cron or first request after the deadline)
2026-01-10 10:05 UTC  publish         → 403 SITE_EXPIRED
2026-01-10 10:06 UTC  reactivate      status=active   expires_at=2026-01-17 10:06