Testers reference

Config

Using Testers is the same ground with the calls around it. Refusals carry an ordinary error code.

testers({
  baseUrl: "https://api.example.com",
  cohortDefaults: {
    targetSize: 12,
    windowDays: 14,
    maxRosterSize: 100,
  },
  nudges: { cooldownHours: 72 },
  optInLinkTtlDays: 30,
})

basePath defaults to /testers.

Every constant behind the forecast is a config field, including the survival priors and the health weights. They are numbers somebody chose, not values fitted to data, and the API says so in a calibration field. Change them and bump modelVersion, so a trend chart annotates where the change took effect.

Roster states

invitedAsked
acceptedSaid yes. Consent, not enrollment
opted_inFollowed the store link through
lapsed
removedTaken off the roster
withdrawnThey opted out themselves

Activity states

activeOpened the app recently
inactiveHas not, and we can tell
never_linkedNever authenticated — we cannot tell. Health is null, and a UI renders this gray, never red
unreachableMail is not getting through

Nudge kinds

confirm · store · inactive · closing

All four carry an opt-out link.

Routes

RouteVerified by
GET /testers/confirm/:tokenpublic — the invitation link
GET /testers/opt-in/:tokenpublic
GET, POST /testers/opt-out/:tokenpublic — both verbs
GET /testers/statususer
GET /testers/cohortscontrol-plane
POST /testers/invitecontrol-plane
POST /testers/resendcontrol-plane
POST /testers/removecontrol-plane
POST /testers/nudgecontrol-plane

Opting out answers both verbs because a one-click unsubscribe header sends a POST and a person clicking the link sends a GET. Refusing either would refuse somebody trying to leave.

Removal is a POST with a body, not a path parameter — an address in a path is an address in every access log, proxy and trace between the client and the Worker.

Scopes

testers:roster:read · testers:roster:write · testers:nudge:send

Error codes

Code
testers/not_configured
testers/cohort_not_found
testers/cohort_closed
testers/member_not_found
testers/already_on_roster
testers/withdrawn
testers/roster_full
testers/invalid_token
testers/nudge_cooldownThe per-tester cooldown refused it
testers/copy_not_allowed

Which fields are estimates

Why the count is an estimate is the reasoning; what the store APIs withhold is why it has to be.

Every one named estimated. Plus:

Field
successProbabilityNullable — we do not know is representable
reconciliation.supportedAlways false, with the reason attached
disclaimerRequired on every response

Days are counted in UTC, and the response says so.

resetPolicy defaults to reset — deliberately pessimistic, because being told day fourteen while actually on day three is the expensive mistake.

What provisioning creates

One Worker, hosting the daily pass, bound as TESTERS_DAILY. No bucket, no index, no secret.

The store’s own limits

Requirement12 testers, 14 continuous days, closed testing only, personal accounts only
Closed-test list limits2,000 users per list · 50 lists per track · 200 lists per account
The “100 testers” figureInternal testing only. Not this

The roster cap of 100 is a management judgment, not a store limit.

ESC