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.
| Event | Resets the clock? | Why |
|---|---|---|
POST /v1/notifications accepted (2xx) | Yes | This is the only activity that counts. |
| Publish where every push submission failed | Yes | The request itself was valid and authorised; per-subscription outcomes are a delivery concern, reported separately. |
| Publish with zero subscribers | Yes | Authorised, well-formed activity. Response warns NO_ACTIVE_SUBSCRIPTIONS. |
| Subscription created, refreshed or removed | No | Subscriber traffic is not a publish. |
GET /health, GET /v1/sites/status | No | Reads never count. |
| Failed publish (400/403/413/429/5xx) | No | A rejected request is not a trigger. |
POST /v1/sites/reactivate | Yes | Explicitly 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
- At request time. Every authenticated request compares
expires_atwith the current UTC time. If the window has elapsed the registration is markedexpiredin the same request and the API returns403 SITE_EXPIRED. This is the authoritative check — it cannot be skipped, scheduled task or not. - 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
| Operation | Expired registration |
|---|---|
POST /v1/notifications | Blocked — 403 SITE_EXPIRED |
POST /v1/subscriptions | Blocked — new subscribers are not collected |
GET /v1/sites/status, lists, counts | Allowed (read-only inspection) |
POST /v1/sites/reactivate | Allowed — this is the recovery path |
POST /v1/sites/rotate-key | Allowed |
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_atrecords 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_versionincrements;key_rotated_atis 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 status | Set when | Effect |
|---|---|---|
active | Created, refreshed, or re-subscribed | Included in publishes. |
invalid | Push service returned 404 or 410, or the stored auth secret failed to decrypt | Excluded from future publishes. The operator sees the count in the delivery report. |
unsubscribed | DELETE /v1/subscriptions | Excluded. 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
| Data | Variable | Default |
|---|---|---|
| Notifications (and their delivery attempts, by cascade) | NOTIFICATION_RETENTION_DAYS | 90 days |
| Delivery attempts | DELIVERY_ATTEMPT_RETENTION_DAYS | 30 days |
| Unsubscribed subscriptions | UNSUBSCRIBED_RETENTION_DAYS | 30 days |
| Rate-limit counters | RATE_LIMIT_RETENTION_HOURS | 24 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