Skip to main content
When a listing receives more applications than available units, Bloom uses a random lottery to determine applicant ranking. The lottery assigns each non-duplicate application a randomized ordinal position. Preference-specific sub-rankings are computed for applicants who qualify for listing preferences (e.g., Live/Work in the City). Partners export these ranked lists to begin the leasing process.

Lottery workflow

1

Listing closes

When a listing’s applicationDueDate passes (or a partner manually closes it), the listing status moves to closed. The reviewOrderType must be set to lottery for the lottery workflow to apply.
2

Resolve flagged sets

Before running the lottery, partners should resolve any flagged duplicate application sets. Applications with markedAsDuplicate: true are automatically excluded from lottery randomization.
3

Generate lottery results

An admin calls PUT /lottery/generateLotteryResults (or uses the Partners Portal). The service:
  1. Deletes any existing applicationLotteryPositions and applicationLotteryTotal records for the listing (to support re-runs)
  2. Fetches all non-deleted, non-duplicate applications
  3. Assigns a random ordinal to each application via lotteryRandomizer()
  4. Stores positions in applicationLotteryPositions
  5. Computes preference-specific sub-rankings for each MultiselectQuestion with section preferences
  6. Sets lotteryStatus to ran
4

Review results

Partners download the lottery export via GET /lottery/getLotteryResults to review the ranked list before publishing. The export includes per-application ordinal ranks and preference sub-ranks.
5

Publish results

Partners or admins call PUT /lottery/lotteryStatus with lotteryStatus: publishedToPublic. This makes results visible to applicants on the public portal. The lotteryLastPublishedAt timestamp is recorded on the listing. Notification emails are sent to applicants.
6

Applicants view results

Applicants use GET /lottery/publicLotteryResults/:id (where id is their application UUID) to see their lottery position across all preference pools and the general pool. GET /lottery/lotteryTotals/:id (where id is the listing UUID) returns total applicant counts per preference pool.

Lottery statuses

The LotteryStatusEnum (from @prisma/client) tracks the lottery’s progress independently of the listing status:
StatusDescription
ranLottery has been generated; results are not yet public
approvedResults have been reviewed and approved internally
publishedToPublicResults are visible to applicants
expiredLottery data has been expired per the LOTTERY_DAYS_TILL_EXPIRY schedule
erroredLottery generation encountered an error
retractedPreviously published results have been retracted
The lottery can be re-run after it has already been run (for example, if new applications were received or if the initial run errored). The rerun and retracted states are tracked in the activity log as LotteryActivityLogStatus values.

Lottery preferences

Preferences are multiselect questions assigned to a listing with applicationSection: preferences. When the lottery is generated, the service computes a separate ranked sub-list for each preference:
  1. All applications that selected the preference option are extracted
  2. They are sorted by their general lottery ordinal (already randomized)
  3. A preference-specific applicationLotteryPositions record is created with the multiselectQuestionId set
This means an applicant may appear at position 47 in the general pool but position 3 in the Live/Work preference pool if they qualified for that preference.
When enableGeocodingPreferences is active on the jurisdiction, preference eligibility can be verified automatically using the applicant’s geocoded address. Only applicants whose address falls within the defined geographic boundary are counted as qualifying for the preference.
The enableGeocodingRadiusMethod flag enables a radius-based alternative: the applicant’s address is compared against a center point and a mile radius rather than a polygon boundary.

Waitlist vs. open lottery

The standard lottery applies to listings with reviewOrderType: lottery. All eligible applications are randomized together. Results are ranked 1 through N.

Auto-publishing results

Bloom includes an automated publish cron job controlled by LOTTERY_PUBLISH_PROCESSING_CRON_STRING. When triggered, PUT /lottery/autoPublishResults finds listings whose lottery results meet the criteria for automatic publication and sets their lotteryStatus to publishedToPublic. This job requires admin or jurisdictional admin authorization and is typically scheduled via environment configuration.

Lottery data expiry

Lottery data is automatically expired after a configurable number of days using the LOTTERY_DAYS_TILL_EXPIRY environment variable. The expiry cron runs on the schedule defined by LOTTERY_PROCESSING_CRON_STRING. When the cron runs (PUT /lottery/expireLotteries), it:
  1. Finds all closed lottery listings whose closedAt date is older than LOTTERY_DAYS_TILL_EXPIRY days
  2. Creates a snapshot of each listing via SnapshotCreateService before expiring
  3. Sets lotteryStatus to expired on all qualifying listings
  4. Writes an activity log entry for each expired lottery
Once a lottery expires, the ranked position data is no longer returned to applicants via the public endpoints. Set LOTTERY_DAYS_TILL_EXPIRY based on your jurisdiction’s data retention policy. If the variable is not set, the expiry cron will not run.

Activity log

Every lottery status change is recorded in the activity log with module lottery. The GET /lottery/lotteryActivityLog/:id endpoint returns the full audit trail for a listing’s lottery, including who made each status change and when. Activity log statuses tracked (LotteryActivityLogStatus): ran, approved, publishedToPublic, expired, errored, rerun, retracted, closed.

API endpoints

MethodPathDescription
PUT/lottery/generateLotteryResultsGenerate (or re-generate) lottery results for a listing
PUT/lottery/lotteryStatusChange the lottery status for a listing
GET/lottery/getLotteryResultsExport lottery results as a ZIP (requires partner auth)
GET/lottery/getLotteryResultsSecureExport lottery results as a signed URL
GET/lottery/publicLotteryResults/:idGet an applicant’s lottery positions by application UUID
GET/lottery/lotteryTotals/:idGet total applicant counts per pool for a listing
GET/lottery/lotteryActivityLog/:idGet the lottery activity log for a listing
PUT/lottery/autoPublishResultsTrigger the auto-publish cron job
PUT/lottery/expireLotteriesTrigger the lottery expiration cron job
PUT /lottery/generateLotteryResults requires the requesting user to have the isAdmin role. Only system administrators can initiate lottery generation.

Lottery exports for partners

The lottery export (GET /lottery/getLotteryResults) reuses the application exporter with lottery-specific flags enabled. The export:
  • Includes all applications (not just those with positions)
  • Adds lottery ordinal rank columns to the output
  • Adds per-preference sub-rank columns for each preference attached to the listing
  • Requires an API key and partner-level permissions via ExportLogInterceptor
The secure export (GET /lottery/getLotteryResultsSecure) returns a signed URL for deferred download.

Build docs developers (and LLMs) love