Skip to content

Auction Frontend Integration — Current API Contract

For the focused migration from the immediately preceding realtime contract, including copy-ready types and acceptance tests, see Auction current changes — complete frontend integration.

This is the single authoritative frontend integration reference for the auction lifecycle, consolidating the REST and WebSocket contracts for the strict, database-authoritative auction execution model.

Highlights of the model:

  • One persisted status axis, auctionStatus, drives the whole lifecycle.
  • The prebid window is a derived prebidPhase, not a persisted state.
  • The pre-auction "review" stage is gone. When the policy requires it, an approval gate (approvalRequired) sits between SCHEDULED and LIVE.
  • Prebid amounts are sealed: no winner can be declared before the live auction ends, and prebids are only a fallback pool when no live bids were received.
  • Live bids are committed synchronously under a PostgreSQL row lock. The auction.bid.place response is the terminal result for that bid request.
  • Automatic lifecycle timing is stored in PostgreSQL. The frontend does not schedule, repair, or poll individual lifecycle jobs.
  • The old reviewStatus, availabilityPhase, liveStartMode, the four decision booleans, and the live-decision endpoints are removed.

This page is the full contract. Companion deep dives (linked inline) cover individual areas in more detail.

Related docs:

Frontend architecture at a glance

REST bootstrap / reconnect snapshot
                 │
                 ▼
       local auction-room state
                 ▲
                 │ ordered deltas (serverSequence / stateVersion)
WebSocket subscribe ───────────────────────────────────────────────┐
                 │                                                │
                 ├── auction.bid.place ── terminal ack/error       │
                 │          │                                     │
                 │          └── PostgreSQL transaction + row lock │
                 │                                                │
                 └── staff commands ── async command events ──────┘

PostgreSQL nextTransitionAt ── per-auction delayed job ── lifecycle transition

The public contract is REST state plus WebSocket acknowledgements/events. nextTransitionAt, delayed jobs, the reconciler, BullMQ job names, and worker retries are internal implementation details. Never expose them in UI state or build client timers that attempt to execute lifecycle transitions.

Required client responsibilities

  1. Bootstrap the screen from REST, then subscribe to the auction room.
  2. Keep the highest applied serverSequence and the latest stateVersion.
  3. Generate one stable request id for each bid intent.
  4. Treat the correlated bid ack or error as terminal. If delivery is uncertain, retry the identical bid with the identical id.
  5. Re-subscribe with lastSequence after reconnect, replace aggregate state from the snapshot, and reconcile the activity feed from its replay parcel.
  6. Use server timestamps for countdown display only. The server, not the browser clock, decides whether an action is still legal.

1. The three rules

  1. No winner selection before the live auction ends. Winner actions (auction.winner.declare, auction.winner.record_lot) are rejected while the auction is SCHEDULED / READY. Winner selection happens only after auctionStatus === ENDED.
  2. A closed prebid window is a pure go-live gate. Once prebidPhase === CLOSED, the only staff path forward is starting the live round — approved first when the policy requires it. There is no amount review, candidate list, or winner action at that stage.
  3. Prebid amounts are sealed. Staff see prebid amounts only after the live auction ends; subscribers see them only after staff explicitly reveal them (also only after end). The live auction opens with no prebid-derived base/leading amount.

2. State model

A single persisted auctionStatus drives the lifecycle. The prebid window is derived as prebidPhase from timestamps; the approval gate is a resolved policy flag (approvalRequired). Drive UI off all three.

2.1 auctionStatus (persisted)

Status Meaning
PENDING Auction row created; not yet scheduled
SCHEDULED Start time configured; prebid window active or awaiting close
READY Approval-required auction approved by staff — ready to start
LIVE Live auction running
PAUSED Live auction paused
ENDED Live auction finished — winner selection allowed here
COMPLETE Every division has a declared winner — terminal
CANCELLED Cancelled by staff

READY appears only when the effective policy sets approvalRequired (resolved from the auction's config snapshot). With approvalRequired disabled (the default), auctions start directly from SCHEDULED and READY never appears. Approving an auction without approvalRequired is rejected.

2.2 prebidPhase (derived, not persisted)

Phase Meaning
NONE Program has no prebid
OPEN Window open (now < prebidEndAt or end-of-cycle-day fallback)
CLOSED Window closed (deadline passed or manual close)

The phase is orthogonal to auctionStatus. Whether it matters depends on the caller: a start is gated on CLOSED, prebid placement on OPEN.

2.3 approvalRequired (resolved policy flag)

Resolved per auction from the config snapshot (triggerPolicy.approvalRequired). Exposed on the console overview and in the approve/schedule responses so the UI can render the approval step only when it applies.

2.4 currentPhase (derived summary)

A single human-oriented lifecycle phase returned by every GET .../auctions/:auctionId response (console overview and subscriber auction detail). It is derived deterministically from auctionStatus + prebidPhase + approvalRequired and is safe to render directly as a phase badge/step indicator:

Phase Derivation
PENDING auctionStatus = PENDING and prebid window not open
PREBID_OPEN Any pre-live status (PENDING/SCHEDULED/READY) while the window is open
PREBID_CLOSED SCHEDULED + prebid window closed + no approval required
PENDING_APPROVAL SCHEDULED + prebid window closed + approvalRequired = true
READY_TO_START READY (approved), or SCHEDULED without prebid and without approval
LIVE_BIDDING auctionStatus = LIVE
PAUSED auctionStatus = PAUSED
ENDED auctionStatus = ENDED
COMPLETE auctionStatus = COMPLETE
CANCELLED auctionStatus = CANCELLED

2.5 Lifecycle diagram

PENDING
   │  PATCH /schedule
   ▼
SCHEDULED  (prebid window opens/closes as prebidPhase)
   │
   ├── approvalRequired === false  ───────────────┐
   │                                              │
   ├── approvalRequired === true                  │
   │   └── POST /approve  ──▶ READY ──────────────┤
   │                                              │
   └─────────── start path (either status) ───────┘
                     │   WS auction.status.update { status: "START" }
                     ▼      (or scheduled auto-start)
                  LIVE ──(pause/resume)──▶ PAUSED
                     │
                     └── auction ends (WS auction.end OR delayed lifecycle job)
                     ▼
                  ENDED   ◄── winner selection ONLY here
                     │        live bids exist        → auction.winner.declare from a bid
                     │        no live bids received  → sealed prebids are the fallback pool:
                     │                                  auction.winner.declare {prebidId} or
                     │                                  auction.winner.record_lot (tie at lowest)
                     │
                     ├── POST /prebids/reveal  (optional, subscriber disclosure)
                     ▼
                 COMPLETE (when every division has a declared winner)

2.6 Automatic lifecycle behavior

Automatic transitions use the frozen triggerPolicy in the auction's config snapshot. The backend persists its next due transition and checks due auctions approximately once per second. A process restart does not lose the deadline: overdue rows remain due in PostgreSQL and are handled after recovery.

Current state Automatic action
PENDING None. Scheduling/arming must first move the auction into its scheduled lifecycle.
SCHEDULED / READY Open prebid immediately when configured and not yet materialized; otherwise start at the later start/close time.
LIVE Advance the active call phase, or end at the calculated auction deadline.
PAUSED None. Resume recomputes the next live deadline.
ENDED Reveal prebids when configured, then declare the next eligible winner when configured.
COMPLETE/CANCELLED None; terminal auctions have no next transition.

Approval still wins over automation: autoStart: true cannot start a SCHEDULED auction while approvalRequired: true; staff must approve it to READY. Manual policy flags produce no automatic action for that step.

The backend processes one transition per delayed job. Immediate chains, such as ENDED → reveal prebids → declare winner, therefore remain observable as separate events through distinct zero-delay jobs. A bid auto-extension, pause/resume, schedule or trigger-policy update, approval, manual transition, winner action, or cancellation recomputes or clears the server-side deadline transactionally.

Frontend consequences:

  • Do not assume an automatic transition occurs at the exact millisecond shown by a countdown. Keep controls pending until the event/snapshot confirms it.
  • Do not call a repair endpoint when a countdown reaches zero. No such frontend responsibility exists.
  • Manual and automatic actions publish the same canonical state events. Render from the resulting status/phase, not from who initiated the transition.
  • If a transition event is missed, reconnect snapshot/replay or a REST refetch restores the authoritative state.

3. REST API — staff (company console & superadmin)

Base path: /v2/companies/auctions/:auctionId/... (company scope) or /v2/admin/auctions/:auctionId/... (superadmin, any company via companyId/auth). Both mount the same handlers; only the role and scope differ.

Auth: Bearer company or superadmin token. All responses use the shared { status, code, message, data, error } envelope.

Success example:

{
  "status": "success",
  "code": 200,
  "message": "Auction console overview fetched successfully.",
  "data": {}
}

Error example:

{
  "status": "error",
  "code": 400,
  "message": "Request could not be completed.",
  "error": {
    "type": "BAD_REQUEST",
    "details": null
  }
}

Always branch on the HTTP status and envelope status; do not infer success from the presence of data. Validation errors may return structured error.details.

3.1 List auctions

GET /v2/companies/auctions
GET /v2/admin/auctions

Query params (all optional): pageNumber, pageSize (default 1 / 10, max 100), companyId (superadmin only), programId, cycleId, search (program name / chit ID / company / branch, max 100 chars), auctionStatus (comma list of AuctionStatus values), cycleStatus (comma list), auctionBidMode, auctionClosingMode, prebidMutabilityPolicy, hasPrebid, allowStaffOfflineBids, prebidStartFrom/To, prebidEndFrom/To, auctionStartFrom/To, endedFrom/To, createdFrom/To, cycleNumber, minimumBidAmountMin/Max, totalAmountMin/Max, leadingBidAmountMin/Max, sortField + sortOrder (asc/desc).

sortField values: auction_start_at (default), prebid_end_at, created_at, cycle_number, program_name, total_amount, leading_bid_amount. Default sort: auction_start_at asc with auction ID tie-breaker.

Response data.auctions[] item (key fields):

{
  "auctionId": "auction-id",
  "cycleId": "cycle-id",
  "programId": "program-id",
  "programName": "Gold Chit 2026",
  "chitId": "CHIT-2026",
  "companyId": "company-id",
  "companyName": "Dichit Company",
  "companyBranch": "Kochi",
  "cycleNumber": 3,
  "cycleStatus": "ACTIVE",
  "auctionStatus": "SCHEDULED",
  "prebidPhase": "OPEN",
  "hasPrebid": true,
  "auctionBidMode": "TOTAL_VALUE_DECREASING",
  "auctionClosingMode": "DURATION_MODE",
  "prebidStartAt": "2026-08-13T09:00:00.000Z",
  "prebidEndAt": "2026-08-13T10:00:00.000Z",
  "auctionStartAt": "2026-08-13T11:00:00.000Z",
  "auctionDurationSeconds": 1800,
  "auctionEndAt": "2026-08-13T11:30:00.000Z",
  "startedAt": null,
  "pausedAt": null,
  "endedAt": null,
  "minimumBidAmount": 100,
  "totalAmount": 1000,
  "leadingBidAmount": null,
  "bidCount": 0,
  "activePrebidCount": 2,
  "createdAt": "2026-08-01T05:30:00.000Z",
  "updatedAt": "2026-08-01T05:30:00.000Z"
}

leadingBidAmount is null while prebid amounts are sealed (see §6). See auction-list.md for the full contract.

3.2 Console overview

GET /v2/companies/auctions/:auctionId
GET /v2/admin/auctions/:auctionId

Query params (all optional): bidPageNumber, bidPageSize, prebidPageNumber, prebidPageSize, participantPageNumber, participantPageSize (default 1 / 10).

Key data fields:

Field Type Notes
auctionId, cycleId, cycleNumber, cycleStatus Identity / cycle metadata.
stateVersion number Monotonic; bump on every mutation. Use to detect stale console state.
auctionStatus enum SCHEDULED | READY | LIVE | PAUSED | ENDED | COMPLETE | CANCELLED
prebidPhase enum NONE | OPEN | CLOSED — derived, see §2.2
currentPhase enum lifecycle summary — see §2.4 table
approvalRequired boolean resolved policy gate — whether READY is required before start
auctionBaseAmount number | null null until the first live bid — never seeded from prebids
minimumBidAmount, totalAmount number | null
leadingBid object | null { id, subscriberId, enrolledSubscriberId, amount, createdAt }. Always a live bid.
bidCount, activePrebidCount number
prebidStartAt, prebidEndAt ISO | null window bounds
auctionStartAt, auctionDurationSeconds, auctionEndAt live schedule
startedAt, pausedAt, endedAt ISO | null runtime timestamps
prebidAmountsRevealed boolean drives the visibility matrix
prebidAmountsRevealedAt ISO | null
minimumBidReachedAt ISO | null
programDivisions, declaredWinnerCount, remainingWinnerSlots number
winnerId string | null
winners[] array declared winners; amount null for prebid-sourced winners until reveal; carries selectionSource (PREBID_REVIEW | LIVE_BID_REVIEW | MANUAL_SELECTION | LOT_SELECTION)
liveBids paginated live bid history (amounts always visible)
prebids paginated items[].amount null until staff-visible at ENDED (or revealed)
participants paginated items[] include invoiceStatus, isPaymentCompletedForCycle, isOnline, source (ONLINE/FLOOR), hasBid, hasPrebid, latestBidAmount, isDisqualified, hasWonCycle, wonCycleNumber
announcements[] array { id, auctionId, cycleId, actorId, message, tone, createdAt }
realtimeStats object | null null until LIVE/PAUSED/ENDED; { bidVelocityPerMinute, averageBidIntervalSeconds, uniqueBidders, totalBids }

3.3 Schedule / reschedule

PATCH /v2/companies/auctions/:auctionId/schedule
PATCH /v2/admin/auctions/:auctionId/schedule

Body:

{
  "auctionStartAt": "2026-08-13T11:00:00.000Z",
  "auctionDurationSeconds": 1800,
  "reason": "Rescheduled",
  "...": "optional settings overrides (see auction-lifecycle-config.md)"
}

auctionStartAt and auctionDurationSeconds are required; reason and settings optional. Only valid from SCHEDULED.

Response data:

{
  "auctionId": "auction-id",
  "cycleId": "cycle-id",
  "stateVersion": 2,
  "status": "SCHEDULED",
  "prebidStartAt": "2026-08-13T09:00:00.000Z",
  "prebidEndAt": "2026-08-13T10:00:00.000Z",
  "auctionStartAt": "2026-08-13T11:00:00.000Z",
  "auctionDurationSeconds": 1800,
  "auctionEndAt": "2026-08-13T11:30:00.000Z",
  "prebidPhase": "OPEN"
}

3.4 Approve auction (only when approvalRequired)

POST /v2/companies/auctions/:auctionId/approve
POST /v2/admin/auctions/:auctionId/approve

Body (optional): { "reason": "Proceeding to live round" }

  • When: auctionStatus === SCHEDULED and resolved approvalRequired is true.
  • Effect: auctionStatus → READY; lifecycle re-armed; action log APPROVE written with fromStatus/toStatus/approvalRequired metadata.
  • Response data: { auctionId, cycleId, status: "READY", approvalRequired, auctionStartAt }.
  • Errors:
  • 400 "This auction does not require approval. Approving an auction without the approval-required policy is not allowed."
  • 400 Illegal transition if not SCHEDULED.
  • Approval publishes a durable auction.state.changed event with payload.transition: "approved". Replace lifecycle state from payload.state; the REST response remains the terminal result for the initiating request.

3.5 Cancel auction

POST /v2/companies/auctions/:auctionId/cancel
POST /v2/admin/auctions/:auctionId/cancel

Body (optional): { "reason": "..." }. Sets auctionStatus = CANCELLED, writes action log CANCEL. Returns the mutation envelope (see §3.14).

3.6 Manual prebid open / close

POST /v2/companies/auctions/:auctionId/prebid/open
POST /v2/admin/auctions/:auctionId/prebid/open
POST /v2/companies/auctions/:auctionId/prebid/close
POST /v2/admin/auctions/:auctionId/prebid/close

Body (optional): { "reason": "..." }.

  • open: materializes the window (prebidStartAt = now, prebidEndAt = deadline or end-of-cycle day). Idempotent; 400 if the window already closed ("Prebid window has already closed and cannot be reopened manually.").
  • close: pins prebidEndAt = now, immediately blocking new prebid submissions. Idempotent. prebidPhase derives to CLOSED.
  • Response data includes prebidStartAt / prebidEndAt and booleans prebidOpened / prebidClosed (false when the action was an idempotent no-op). Publishes auction.state.changed with transition: "prebid_opened" / "prebid_closed". Action logs MANUAL_OPEN_PREBID / MANUAL_CLOSE_PREBID.

Full contract: manual-prebid-controls.md.

3.7 Reveal prebid amounts (subscriber disclosure)

POST /v2/companies/auctions/:auctionId/prebids/reveal
POST /v2/admin/auctions/:auctionId/prebids/reveal

Body (optional): { "reason": "..." }.

  • When: auctionStatus === ENDED only — 400 "Prebid amounts can be revealed only after the auction ends." otherwise.
  • Stamps prebidAmountsRevealedAt, publishes auction.state.changed { transition: "prebids_revealed" }. Action log REVEAL_PREBIDS.

3.8 Action logs (audit trail)

GET /v2/companies/auctions/:auctionId/action-logs
GET /v2/admin/auctions/:auctionId/action-logs

data.actionLogs[] with action ∈ AUCTION_ACTION_TYPES:

SCHEDULE, RESCHEDULE, START, AUTO_START, PAUSE, RESUME, END, AUTO_END,
DECLARE_WINNER, DISQUALIFY_WINNER, REPLACE_DISQUALIFIED_WINNER, RECORD_LOT,
APPROVE, CANCEL, DELETE_BID, DELETE_PREBID, DISQUALIFY_BIDDER, REVEAL_PREBIDS,
UPDATE_SETTINGS, STAFF_OFFLINE_BID, MANUAL_OPEN_PREBID, MANUAL_CLOSE_PREBID,
AUTO_OPEN_PREBID, AUTO_REVEAL_PREBIDS, AUTO_DECLARE_WINNER, ARM_LIFECYCLE

(Removed from the vocabulary: APPROVE_LIVE_AUCTION, DISQUALIFY_PREBID_CANDIDATE, DISQUALIFY_LIVE_BID_CANDIDATE.)

3.9 Presence events

GET /v2/companies/auctions/:auctionId/audience-sessions
GET /v2/admin/auctions/:auctionId/audience-sessions

Query: enrolledSubscriberId, eventType (JOIN | LEAVE), pageNumber/pageSize.

3.10 Eligible winner candidates

GET /v2/companies/auctions/:auctionId/eligible-winner-candidates
GET /v2/admin/auctions/:auctionId/eligible-winner-candidates

Query: pageNumber, pageSize. Returns eligible subscribers (enrolledSubscriberId, subscriberId, name, avatar, email, mobileNumber) for the manual-declare picker. No prebid amounts.

3.11 Replace winner (post-end)

There is no PATCH /winner endpoint. To replace a declared winner, disqualify the current one (POST .../winner/disqualify, §3.12) and then declare the next candidate via the WebSocket auction.winner.declare command (mode CANDIDATE with a different bidId / prebidId).

3.12 Disqualify winner (post-end)

POST /v2/companies/auctions/:auctionId/winner/disqualify
POST /v2/admin/auctions/:auctionId/winner/disqualify

Body:

{
  "winnerId": "winner-id",
  "disqualificationReasonCode": "KYC_ISSUE",
  "disqualificationNote": "Invalid document",
  "reason": "Disqualifying winner"
}

disqualificationReasonCode ∈ KYC_ISSUE | PAYMENT_ISSUE | ELIGIBILITY_ISSUE | RULE_VIOLATION | OTHER (winnerId, disqualificationReasonCode required). Action log DISQUALIFY_WINNER.

3.13 Delete prebid

DELETE /v2/companies/auctions/:auctionId/prebids/:prebidId
DELETE /v2/admin/auctions/:auctionId/prebids/:prebidId

Deletes a specific prebid (no body). Action log DELETE_PREBID. Returns the mutation envelope (see §3.14).

3.14 Mutation response envelope

Most staff auction actions return this shared mutation data shape; fields are null unless relevant to the action. Schedule (§3.3) and trigger-policy override (§3.17) have their own response shapes.

{
  "auctionId": "auction-id",
  "cycleId": "cycle-id",
  "stateVersion": 8,
  "status": "LIVE",
  "approvalRequired": false,
  "leadingBidId": "bid-id",
  "leadingBidAmount": 25000,
  "prebidAmountsRevealed": false,
  "prebidAmountsRevealedAt": null,
  "prebidAmountsRevealedById": null,
  "prebidStartAt": "2026-08-05T10:00:00.000Z",
  "prebidEndAt": "2026-08-10T18:30:00.000Z",
  "prebidOpened": null,
  "prebidClosed": null,
  "bidId": null,
  "prebidId": null,
  "disqualificationId": null,
  "enrolledSubscriberId": null,
  "phase": null,
  "winnerId": null,
  "winningBidId": null,
  "division": null
}

Use stateVersion to detect stale console state; the realtime stream is the authoritative source for live updates.

3.15 Resolved company auction policy

GET /v2/companies/company-auction-policy/resolved
GET /v2/admin/companies/:companyId/company-auction-policy/resolved

Folds default < platform < company. Response data carries settings, allowedSecurities, triggerPolicy (now including approvalRequired), and provenance (source of every resolved key). Use it to prefill program-creation forms. See auction-lifecycle-config.md.

3.16 Config & settings APIs (approvalRequired / triggerPolicy)

approvalRequired is a config flag, so every settings/policy write surface accepts it and every settings/policy read surface returns it. It can be set per preset, per company policy, per platform policy, and per program default settings, and folds as default < platform < company with program settings as the final override.

The triggerPolicy object (5 automated-lifecycle flags, returned on all settings/policy reads and accepted by program creation step 2):

{
  "autoStart": true,
  "approvalRequired": false,
  "autoOpenPrebid": true,
  "autoRevealPrebids": false,
  "autoDeclareWinner": false
}

Write bodies (platform policy PUT, company policy PUT, preset create/update, program settings PATCH, program creation step 2) accept approvalRequired as a top-level key alongside the other config-layer fields (autoStart, autoOpenPrebid, autoRevealPrebids, autoDeclareWinner, auctionBidMode, auctionClosingMode, auctionDurationSeconds, auctionExtensionSeconds, auctionFirstCallSeconds, auctionSecondCallSeconds, auctionThirdCallSeconds, subscriberJoinGraceSeconds, allowStaffOfflineBids, prebidMutabilityPolicy, auctionJoiningRule, allowedSecurities).

Endpoint map (all Bearer staff token, shared response envelope):

Method Path Applies to
GET /v2/admin/platform-auction-policy platform policy
PUT /v2/admin/platform-auction-policy platform policy
GET /v2/companies/company-auction-policy company policy
PUT /v2/companies/company-auction-policy company policy
GET /v2/admin/companies/:companyId/company-auction-policy company policy
PUT /v2/admin/companies/:companyId/company-auction-policy company policy
GET /v2/companies/company-auction-policy/resolved resolved policy
GET /v2/admin/companies/:companyId/company-auction-policy/resolved resolved policy
GET /v2/companies/auction-presets / POST preset CRUD
GET /v2/companies/auction-presets/:presetId / PUT / DELETE preset CRUD
GET /v2/admin/auction-presets / POST preset CRUD
GET /v2/admin/auction-presets/:presetId / PUT / DELETE preset CRUD
GET /v2/admin/companies/:companyId/auction-presets / POST preset CRUD
GET /v2/admin/companies/:companyId/auction-presets/:presetId / PUT / DELETE preset CRUD
GET /v2/companies/programs/:programId/settings program settings
PATCH /v2/companies/programs/:programId/settings program settings
GET /v2/admin/programs/:programId/settings program settings
PATCH /v2/admin/programs/:programId/settings program settings
POST /v2/companies/programs/new/.../steps/2 (and admin variant) program creation step 2
POST /v2/companies/programs/existing/:programId/steps/2 (and admin variant) program creation step 2

Program default settings (GET|PATCH .../programs/:programId/settings) response data:

{
  "programId": "program-id",
  "programName": "Auctions",
  "presetId": "preset-id",
  "prebidDocumentRequired": true,
  "prebidDocument": null,
  "settings": { "...": "all auction settings" },
  "allowedSecurities": ["CHIT"],
  "triggerPolicy": {
    "autoStart": true,
    "approvalRequired": false,
    "autoOpenPrebid": true,
    "autoRevealPrebids": false,
    "autoDeclareWinner": false
  },
  "provenance": { "approvalRequired": "company", "autoStart": "platform", "...": "..." }
}

provenance reports, per config key, which layer (default | platform | company | preset | program | auction) supplied the value. Reads that return triggerPolicy/provenance now include approvalRequired in both.

See auction-lifecycle-config.md for the full contract of the config-layer endpoints.

3.17 Per-auction trigger-policy override

PATCH /v2/companies/auctions/:auctionId/trigger-policy
PATCH /v2/admin/auctions/:auctionId/trigger-policy

Use this route to override automation for one auction without changing the program/company defaults. It is legal only while the cycle is ACTIVE and the auction is PENDING, SCHEDULED, or READY.

The body is strict; every flag is optional and nullable. A boolean sets the auction override, while null clears that flag's override and restores its resolved inherited value.

{
  "autoStart": true,
  "approvalRequired": false,
  "autoOpenPrebid": true,
  "autoRevealPrebids": false,
  "autoDeclareWinner": false,
  "reason": "Use automatic execution for this cycle"
}

Response data:

{
  "auctionId": "auction-id",
  "cycleId": "cycle-id",
  "status": "SCHEDULED",
  "configVersion": 4,
  "triggerPolicy": {
    "autoStart": true,
    "approvalRequired": false,
    "autoOpenPrebid": true,
    "autoRevealPrebids": false,
    "autoDeclareWinner": false
  },
  "provenance": {
    "autoStart": "auction",
    "approvalRequired": "program"
  }
}

The backend freezes the resolved policy, increments the config version, and recomputes lifecycle timing in the same operation. It then publishes auction.triggerPolicy.updated. Replace the local policy with the returned or event value; do not merge it with a stale client-side inheritance calculation.


4. REST API — subscriber

Base path: /v2/subscribers/me/auctions. Auth: Bearer subscriber token. Same { status, code, message, data, error } envelope.

4.1 List auctions

GET /v2/subscribers/me/auctions?pageNumber=1&pageSize=10&search=...
GET /v3/subscribers/me/auctions?pageNumber=1&pageSize=10        (mobile, no `search`)

data.auctions[] item: auctionId, cycleId, enrolledSubscriberId, programId, programName, chitId, companyId, companyName, companyAvatar, companyBranch, cycleNumber, cycleStatus, auctionStatus, prebidPhase, prebidEndAt, auctionStartAt, auctionDurationSeconds, auctionEndAt, closingPhase, closingPhaseEndsAt, minimumBidAmount, totalAmount, leadingBidAmount (null while sealed / before live), invoiceStatus.

4.2 Auction detail

GET /v2/subscribers/me/auctions/:auctionId

data fields:

Field Type Notes
auctionId, cycleId, cycleNumber, cycleStatus
programId, programName, chitId
prebidDocumentRequired boolean prebid signature document needed
prebidDocumentTemplateVersionId string | null
companyId, companyName, companyAvatar, companyBranch
totalAmount number | null
auctionStatus enum drives live participation
prebidPhase enum NONE | OPEN | CLOSED — drives prebid placement
currentPhase enum always present — lifecycle summary, see §2.4 table
prebidEndAt, auctionStartAt, auctionEndAt, auctionDurationSeconds ISO/number | null
closingPhase enum | null Active FIRST_CALL, SECOND_CALL, or THIRD_CALL.
closingPhaseEndsAt ISO | null Authoritative bid deadline while closingPhase is active.
liveStartedAt ISO | null always present — ISO-8601 live-start marker (null until the auction actually goes live; set once, never cleared)
prebidAmountsRevealed boolean
prebidAmountsRevealedAt ISO | null
leadingBidAmount number | null live-bid-only; null before live and while sealed
minimumBidAmount number | null
settings object resolved settings incl. prebidMutabilityPolicy, auctionJoiningRule
joinWindow object { status, version, opensAt, openedAt, closesAt, closedAt, joiningRule }
audience object | null null before the join window opens; afterward contains counts and a participant page only when the requester has joined
liveBids[] array { id, enrolledSubscriberId, subscriberName, subscriberAvatar, amount, createdAt }
winners[] array populated only when ENDED/COMPLETE; amount null for prebid-sourced winners until reveal
announcements[] array { id, auctionId, cycleId, actorId, message, tone, createdAt }
enrollments[] array per-enrollment invoiceStatus, authoritative currentUser viewing/join/bid gates, own liveBids, and own prebids

4.3 Place prebid

POST /v2/subscribers/me/auctions/:auctionId/prebid

Body: { "enrolledSubscriberId": "...", "amount": 1000, "signatureAssetId": "..." } (enrolledSubscriberId, amount required).

  • Allowed only while prebidPhase === 'OPEN'.
  • One active prebid per enrollment — placing while one is active is rejected; cancel first.
  • If prebidDocumentRequired, a valid signatureAssetId is required.

Response data: { auctionId, cycleId, prebidId, enrolledSubscriberId, status: "ACTIVE", documentSubmissionId }.

4.4 Edit prebid

PUT /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId

Body same as place. Allowed only while the window is open and settings.prebidMutabilityPolicy === 'MUTABLE_UNTIL_CLOSE'. Under IMMUTABLE_CANCEL_ONLY, updating after creation is rejected; cancel and re-submit.

4.5 Cancel prebid

DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId?enrolledSubscriberId=...

Query requires enrolledSubscriberId. Frees the slot for a new prebid while the window is open.

4.6 Cycle-scoped auction detail (v2)

GET /v2/subscribers/me/cycles/:cycleId/auction/:enrolledSubscriberId

The realtime-style payload for the auction room screen (same shape the WS auction.state.snapshot embeds as auction). This is the endpoint whose availabilityPhase field became prebidPhase. Key data fields:

Field Type Notes
auctionId, cycleId, programId, companyId, cycleNumber, cycleStatus
stateVersion number monotonic
auctionStatus enum AuctionStatus values
programBidType enum | null AUCTION
minimumBidAmount, totalAmount number | null
leadingBid object | null { id, subscriberId, enrolledSubscriberId, amount (null while sealed), createdAt } — live-bid-only
bidCount, activePrebidCount number
prebidAmountsRevealed boolean
prebidAmountsRevealedAt ISO | null
prebidStartAt, prebidEndAt, auctionStartAt, auctionEndAt, minimumBidReachedAt, startedAt, pausedAt, endedAt ISO | null
auctionDurationSeconds integer | null
prebidPhase enum NONE | OPEN | CLOSED — replaced availabilityPhase
settings object resolved settings
auctionBaseAmount number | null null until the first live bid — never seeded from prebids
liveBids[] array { id, subscriberId, enrolledSubscriberId, subscriberName, subscriberAvatar, amount (null while sealed), type: "PREBID" \| "LIVE", createdAt }
currentUser object Includes canViewDetails, canSubscribeRealtime, separate PREBID/LIVE disqualification flags, canPrebid, prebidBlockedReason, join/bid state, own bid/prebid state, and disqualifications[].

4.7 Cycle overview (v2 + v3)

GET /v2/subscribers/me/cycles/:cycleId/overview/:enrolledSubscriberId
GET /v3/subscribers/me/cycles/:cycleId/overview/:enrolledSubscriberId

The cycle dashboard. Auction state lives under data.bidOverview (absent/null when the cycle has no auction):

Field Type Notes
bidOverview.auctionId string
bidOverview.prebidPhase enum NONE | OPEN | CLOSED — replaced availabilityPhase
bidOverview.auctionStatus enum AuctionStatus values
bidOverview.auctionBaseAmount number | null null until first live bid
bidOverview.minimumBidAmount number | null
bidOverview.leadingBid object | null { amount } — live-bid-only
bidOverview.totalBidCount number | null
bidOverview.prebidEndAt, auctionStartAt, auctionDurationSeconds, auctionEndAt ISO/number | null
bidOverview.currentUser object isLeadingBidder, payment/winner state, isPrebidDisqualified, canPrebid, prebidBlockedReason, activePrebid { amount }, latestBid { amount }

Other overview fields: cycleId, cycleNumber, cycleStatus, startDate, endDate, auctionStartAt, auctionDurationSeconds, installmentAmount, delayFineAmount, invoice fields (invoiceId, invoiceStatus, invoiceMarkedType, ...), cycleWinners[], cycleTransactions[], currentUser, totalPaid, amountToBePaid.


5. WebSocket

Endpoint GET /ws. Auth via sec-websocket-protocol header (websocket.v1, bearer.<token>). Rooms are keyed by auctionId (auction:<auctionId>, staff additionally auction:staff:<auctionId>). Fetch auctionId via REST before subscribing (auction row guaranteed before READY/LIVE).

5.1 Subscribe / unsubscribe (all roles)

Type Payload
auction.subscribe { auctionId, enrolledSubscriberId? (subscriber), lastSequence? }
auction.unsubscribe { auctionId }
auction.participation.join { auctionId, enrolledSubscriberId? }; message id is required and idempotent

Subscribe replies with auction.state.snapshot (+ optional replay), auction.audience.snapshot.

Subscribing is viewer access: it requires an eligible enrollment but no payment. Joining is a separate paid action. It opens at T−10 and returns accepted or replayed with participantId, joinedAt, and joinWindowVersion. A durable participant can reconnect after closure and bid until the auction ends.

5.2 Staff commands

All require auction:staff permission and an auctionId. Staff commands are async. The immediate response only means the command was accepted for execution:

{
  "id": "staff-request-001",
  "type": "command.acknowledged",
  "data": {
    "command": "auction.pause",
    "status": "accepted"
  }
}

There is no business result in this acknowledgement. Success is delivered through the canonical room event; asynchronous failures arrive as correlated command.rejected or command.failed events. Keep the control pending until one of those outcomes arrives or a snapshot/refetch proves the new state.

Live controls

Type Payload When legal
auction.status.update { auctionId, status: "START" } SCHEDULED (no approval) or READY (approval)
auction.pause { auctionId, reason } LIVE
auction.resume { auctionId, reason } PAUSED
auction.end { auctionId, reason } LIVE / PAUSED

Start gate: START is rejected with error { code: "BAD_REQUEST" } if the prebid window is open — "Close the prebid window before starting the live auction." — or if the auction requires approval and has not been approved — "Approve this auction before starting the live auction."

Bid moderation

Type Payload
auction.bid.mark { auctionId, action: "DELETE_BID" \| "DISQUALIFY_BIDDER", bidId?, enrolledSubscriberId?, reason, reasonCode? }
  • DELETE_BID → requires bidId; reasonCode optional.
  • DISQUALIFY_BIDDER → requires enrolledSubscriberId + reasonCode.

reasonCode ∈ KYC_ISSUE | PAYMENT_ISSUE | ELIGIBILITY_ISSUE | RULE_VIOLATION | OTHER. (The old DISQUALIFY_CANDIDATE_AND_DECLARE_NEXT action is removed.)

Winner selection (only after the auction ends)

Type Payload Behavior
auction.winner.declare { auctionId, mode: "CANDIDATE", bidId? \| prebidId?, reason? } Declares from an existing live bid or (only when the auction ended with no live bids) from a sealed prebid
auction.winner.declare { auctionId, mode: "MANUAL", enrolledSubscriberId, amount, reason? } Manual declaration of an eligible subscriber (amount required)
auction.winner.record_lot { auctionId, winnerEnrolledSubscriberId, candidateEnrolledSubscriberIds: [≥2], reason? } Resolves a tie at the lowest amount by lot; legal from live bids or from the prebid fallback pool

Both are rejected with 400 before ENDED. Declaring the last division's winner moves the auction to COMPLETE; a prebid-fallback winner on an auction with remaining divisions reopens it to SCHEDULED.

Announcements / presence

Type Payload
auction.announcement.create { auctionId, message, tone: "INFO" \| "WARNING" }
auction.presence.floor.update { auctionId, enrolledSubscriberId, present: boolean }

5.3 Subscriber bid placement

auction.bid.place is the only synchronous auction command. It does not enter a bid inbox or wait for a dispatch worker. The handler validates and commits the bid before replying.

Request:

{
  "id": "bid-request-001",
  "type": "auction.bid.place",
  "data": {
    "auctionId": "auction-1",
    "amountMinor": "150000"
  }
}
Field Required Rules
envelope id Yes Trimmed string, 1–128 characters. This is also the bid command/idempotency ID.
data.auctionId Yes Non-empty auction ID.
data.amountMinor Yes Positive base-10 integer string, max 19 digits and max 9223372036854775807. No decimal or sign.
data.enrolledSubscriberId Subscriber: optional; staff floor bid: needed Selects the enrollment. Subscriber authorization may resolve it; staff must identify the floor bid.

The data object is strict. In particular, do not send expectedAuctionVersion, a client timestamp, a sequence number, or an amount in major-unit decimal form. Unknown fields return BAD_REQUEST.

Convert the UI amount to minor units without floating-point multiplication. For a two-decimal currency, 1500.00 becomes the string "150000". Keep the minor-unit value as a string across the WebSocket boundary.

Accepted response:

{
  "id": "bid-request-001",
  "type": "ack",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-30T12:00:00.000Z",
  "data": {
    "eventType": "auction.bid.place",
    "status": "accepted",
    "commandId": "bid-request-001",
    "auctionId": "auction-1",
    "cycleId": "cycle-1",
    "bidId": "bid-1",
    "amountMinor": "150000",
    "stateVersion": 42
  }
}

data.status is:

  • accepted: a new bid was committed.
  • replayed: the same scoped id and identical bid details were already committed. This is also success; use the returned stored bid identity.

Within the same auction/enrollment scope, reusing the ID with a changed amount or other bid details returns BAD_REQUEST and must not be treated as a retry.

Rejected response:

{
  "id": "bid-request-001",
  "type": "error",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-30T12:00:00.000Z",
  "data": {
    "code": "BAD_REQUEST",
    "message": "The bid does not satisfy the current auction rules."
  }
}

The bid is checked against the latest committed auction state inside one transaction: auction/cycle status, participation, payment, prior program win, disqualification, staff floor-bid permission, amount rules, current leader, closing phase, and auto-extension. A successful commit inserts the bid, updates the leader and stateVersion, persists activity events, and updates the next lifecycle deadline atomically.

Concurrent bids are serialized by PostgreSQL's auction-row lock. Lock acquisition order is authoritative; browser send time, client clock, and expectedAuctionVersion do not establish priority. Five or ten simultaneous requests are not silently discarded: while the connections remain available, each receives an accepted, replayed, or explicit error result. A later request that is no longer valid after an earlier commit is explicitly rejected.

Bid retry state machine

READY_TO_SEND
   │ create stable id + freeze payload
   ▼
SUBMITTED ── ack accepted/replayed ──▶ COMMITTED
   │
   ├── correlated business/validation error ──▶ REJECTED
   │
   └── timeout / disconnect / INTERNAL uncertainty ──▶ UNCERTAIN
                                                       │
                                  retry same id+payload │
                                                       ▼
                                                terminal result

Never generate a new ID for an uncertain retry: that would represent a second bid intent and could create another valid bid. Never assume a bid failed just because auction.bid.created did not arrive. The terminal acknowledgement is authoritative; realtime publication is post-commit and best effort.

For each locally pending bid, store at least { id, auctionId, amountMinor, enrolledSubscriberId, submittedAt } until terminal resolution. Disable only the duplicate submission of that same intent; the product may still allow the user to intentionally submit a new bid with a new ID after the first resolves.

5.4 Server events

Every canonical auction broadcast uses this outer shape:

{
  "id": "event-id",
  "type": "auction.bid.created",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-30T12:00:00.000Z",
  "correlationId": "bid-request-001",
  "causationId": "bid-request-001",
  "data": {
    "auctionId": "auction-1",
    "cycleId": "cycle-1",
    "stateVersion": 42,
    "serverSequence": 108,
    "requestId": "bid-request-001",
    "bidId": "bid-1",
    "leadingBidAmount": 1500,
    "payload": {}
  }
}

Fields can be absent when an event does not use them. serverSequence orders the durable auction activity stream. stateVersion orders auction aggregate mutations. correlationId/requestId may connect an event to the originating command but must not replace the terminal bid acknowledgement.

Every newly persisted lifecycle event uses one canonical payload. The live broadcast and the replayed copy have the same payload shape:

interface AuctionLifecyclePayload {
  transition:
    | 'schedule_changed'
    | 'prebid_opened'
    | 'prebid_closed'
    | 'prebids_revealed'
    | 'approved'
    | 'started'
    | 'paused'
    | 'resumed'
    | 'extended'
    | 'call_started'
    | 'closed'
    | 'cancelled'
    | 'winner_selected'
    | 'winner_disqualified';
  state: {
    status: AuctionStatus;
    stateVersion: number;
    currentPhase: AuctionCurrentPhase;
    prebidPhase: 'NONE' | 'OPEN' | 'CLOSED';
    auctionStartAt: string | null;
    prebidStartAt: string | null;
    prebidEndAt: string | null;
    auctionEndAt: string | null;
    startedAt: string | null;
    pausedAt: string | null;
    closingPhase: 'FIRST_CALL' | 'SECOND_CALL' | 'THIRD_CALL' | null;
    closingPhaseEndsAt: string | null;
    prebidAmountsRevealed: boolean;
    prebidAmountsRevealedAt: string | null;
    approvalRequired: boolean;
    cycleStatus: 'UPCOMING' | 'ACTIVE' | 'ENDED';
    joinWindow: AuctionJoinWindow;
  };
  winner?: PublicDeclaredWinner;
  declaredWinnerCount?: number;
  remainingWinnerSlots?: number;
  replacementRequired?: boolean;
}

Use payload.state as a replacement lifecycle slice. Do not reconstruct the state from transition-specific fields at the payload root; those older fields are no longer emitted by the updated command paths.

The room broadcasts auction.state.changed with data.payload.transition including:

Transition Trigger
schedule_changed Auction timing/settings were rescheduled.
prebid_opened / prebid_closed Manual or automatic prebid-window transition.
started Live auction started manually or automatically; payload also carries startedAt.
paused / resumed Pause or resume.
extended A committed bid extended the live deadline.
call_started A duration/call-phase closing step began.
closed Auction ended and entered ENDED.
prebids_revealed Manual or automatic prebid disclosure.
approved Approval-required auction moved from SCHEDULED to READY.
winner_selected Winner committed; payload status is ENDED, SCHEDULED, or COMPLETE as applicable.
winner_disqualified Declared winner disqualified; may reopen COMPLETE to ENDED.
cancelled Auction cancelled.

Other server events: auction.state.snapshot, auction.settings.updated, auction.triggerPolicy.updated, auction.bid.created, auction.bid.updated, auction.bid.deleted, auction.prebid.created, auction.prebid.deleted, auction.announcement.created/updated/deleted, auction.bidder.disqualified, auction.audience.changed, auction.join_window.opened, auction.join_window.closed, and the staff-only auction.participation.joined and auction.winner.changed events. auction.prebid.created is also staff-only and contains the full prebid projection with a redacted amount when sealed.

auction.winner.changed is the staff console delta for selection and disqualification. Its payload is { action: 'SELECTED' | 'DISQUALIFIED', winner, suggestedReplacementCandidate? }; the winner includes staff identity, selection, replacement, and disqualification audit fields.

auction.bid.created adds the committed bid. When it replaces a leader, auction.bid.updated marks the previous bid as outbid. Apply activity events in ascending serverSequence; ignore an already-applied sequence. Sequences are allocated globally per auction but replay is filtered by role, scope, and targetUserId, so a subscriber's visible sequence values may legitimately skip staff-only events. Do not infer packet loss from a numeric gap alone; reconnect from the last high-water cursor when transport loss or stale state is detected.

5.5 Snapshot, replay, and reconnect

On initial subscription, the server first acknowledges auction.subscribe, then pushes auction.connected, auction.state.snapshot, and auction.audience.snapshot. Treat the state snapshot as replacement state, not as a patch.

On reconnect:

  1. Open a new authenticated socket.
  2. Send auction.subscribe with the highest durably applied lastSequence and, for a subscriber, its enrolledSubscriberId.
  3. Replace aggregate auction state with snapshot.data.payload.auction.
  4. When replay is present, keep the feed that existed through the requested cursor, discard the snapshot's recentBids tail, and apply visible snapshot.data.payload.replay.events in ascending sequence. The list may be sparse because staff-only and other-user-targeted events are filtered.
  5. Set the cursor to snapshot.data.payload.replay.lastSequence. On a fresh subscription without replay, build the feed from recentBids and use the snapshot's lastSequence.
  6. If replay is unavailable, malformed, or produces inconsistent state, refetch the REST detail and subscribe again from its fresh room state.

Do the same after a tab returns from a long background suspension. Presence is ephemeral; always replace it from auction.audience.snapshot.

Join-window and participation events are durable and delivered at least once. Deduplicate by event id, apply by serverSequence, and ignore older join-window versions after a reschedule.

5.6 Minimal TypeScript client pattern

interface PendingBid {
  readonly id: string;
  readonly auctionId: string;
  readonly amountMinor: string;
  readonly enrolledSubscriberId?: string;
}

interface BidAck {
  readonly eventType: 'auction.bid.place';
  readonly status: 'accepted' | 'replayed';
  readonly commandId: string;
  readonly auctionId: string;
  readonly cycleId: string;
  readonly bidId: string;
  readonly amountMinor: string;
  readonly stateVersion: number;
}

function sendBid(socket: WebSocket, bid: PendingBid): void {
  socket.send(
    JSON.stringify({
      id: bid.id,
      type: 'auction.bid.place',
      data: {
        auctionId: bid.auctionId,
        amountMinor: bid.amountMinor,
        ...(bid.enrolledSubscriberId
          ? { enrolledSubscriberId: bid.enrolledSubscriberId }
          : {})
      }
    })
  );
}

Route incoming envelopes by both type and id. Resolve a pending bid only when an ack or error carries the same ID. A room event with the same requestId can update the shared auction feed, but does not resolve the request promise.


6. Prebid amount visibility matrix

Viewer Before auction ends (ENDED) After ENDED, before reveal After POST /prebids/reveal
Staff (company + superadmin console) Prebid row amounts null; leadingBid.amount null (leading bid is live-bid-only); winner amount null Prebid amounts visible (fallback review needs them) Visible
Subscriber (own prebid) Their own enrollments[].prebids[].amount visible Own prebid only All amounts visible
Subscriber (others) Hidden Hidden Visible

When hidden, amounts come back as null (fields are not omitted). The console overview exposes prebidAmountsRevealed / prebidAmountsRevealedAt so the UI can mirror this exactly.


7. Error catalog (frontend-facing messages)

REST uses numeric HTTP status codes. WebSocket error.data.code uses one of BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED, or INTERNAL.

WebSocket code Client handling
BAD_REQUEST Terminal for this payload. Show the safe message and let the user correct the action.
UNAUTHORIZED Refresh/re-authenticate, reconnect, then retry only if the outcome was not already terminal.
FORBIDDEN Terminal until authorization/eligibility changes. Do not retry in a loop.
NOT_FOUND Terminal for the referenced ID; refetch navigation data.
CONFLICT Terminal for this payload; refresh authoritative auction state.
RATE_LIMITED Back off and allow a deliberate retry. Preserve a bid's ID and payload if its commit outcome remains uncertain.
INTERNAL Outcome may be uncertain. Reconcile state; for a bid retry the identical ID and payload.

Transport timeout/socket close has no error envelope and therefore does not prove rejection. Mark the operation UNCERTAIN. This distinction is essential for idempotent bidding.

Scenario Code Message
Start while prebid open 400 "Close the prebid window before starting the live auction."
Start from SCHEDULED when approval needed 400 "Approve this auction before starting the live auction."
Approve without approval policy 400 "This auction does not require approval. Approving an auction without the approval-required policy is not allowed."
Winner action before auction ends 400 Illegal transition / "no winner selection" guard
Reveal before end 400 "Prebid amounts can be revealed only after the auction ends."
Reopen a closed prebid window 400 "Prebid window has already closed and cannot be reopened manually."
All divisions declared 400 "All division winners are already declared for this cycle."
Company token on foreign auction 403 "You do not have access to this auction."
Subscriber prebid while window closed 400 Prebid submission rejected outside the OPEN prebid phase.
Subscriber bid before payment / while not LIVE 400 Eligibility guard rejection.

8. Console button-state cheat sheet

Button Show when Enabled when
Open prebid hasPrebid + SCHEDULED prebidPhase === 'OPEN' and prebidStartAt === null
Close prebid hasPrebid + SCHEDULED prebidPhase === 'OPEN'
Approve SCHEDULED approvalRequired === true (hide entirely when false)
Start live SCHEDULED/READY prebidPhase === 'CLOSED' and (approvalRequired === false or auctionStatus === 'READY')
Pause / Resume / End live LIVE / PAUSED / LIVE-or-PAUSED
Declare winner / Lot / Replace ENDED auctionStatus === 'ENDED' (and remainingWinnerSlots > 0 for declare)
Reveal prebids ENDED prebidAmountsRevealed === false

9. Migration notes (from the old model)

  • Bids are synchronous: remove bid-command polling, bid-dispatch status, optimistic expectedAuctionVersion checks, and any UI that interprets an immediate acknowledgement as only "queued". auction.bid.place now returns ack.data.status = accepted | replayed only after the database outcome is known, or a correlated error.
  • Bid request IDs are mandatory idempotency keys: generate one ID per user intent and preserve both ID and payload across uncertain retries. Sending the same ID with different details is an error.
  • No client-visible bid inbox: the bid-command table, dispatch/health/cleanup jobs, and the dedicated bid-dispatch queue are removed. Frontends must not query or display their former command states.
  • Lifecycle deadlines are database-authoritative: deterministic per-auction delayed jobs wake normal open/start/end/reveal/winner transitions. A 30-second reconciler repairs missed jobs by finding due auctions in PostgreSQL. There is no public nextTransitionAt field and no frontend scheduler API.
  • Single status axis: reviewStatus + availabilityPhase + liveStartMode are gone. Drive UI off auctionStatus (+ derived prebidPhase + resolved approvalRequired). For a single phase badge, use the new derived currentPhase on every GET .../auctions/:auctionId response (§2.4).
  • availabilityPhase → prebidPhase everywhere it was exposed: the staff console overview and list, the subscriber auction detail (GET /v2/subscribers/me/auctions/:auctionId), the cycle-scoped auction detail (GET /v2/subscribers/me/cycles/:cycleId/auction/:enrolledSubscriberId), the cycle overview bidOverview (v2 and v3), and the v3 auctions list.
  • Approval is a gate, not a mode: the live-decision endpoint (and its APPROVE_LIVE_AUCTION action log) is removed. Use POST .../approve → READY when approvalRequired is enabled; otherwise start directly from SCHEDULED.
  • approvalRequired is now a config key: added to the platform/company policy, auction-preset, program-settings, and program-creation (step 2) write bodies and to triggerPolicy/provenance on every settings/policy read (see §3.16).
  • Removed endpoints: POST .../prebid/disqualify-and-declare-next and the DISQUALIFY_CANDIDATE_AND_DECLARE_NEXT auction.bid.mark action are gone. The old cycle-scoped company/admin auction routes (/v2/companies|admin/cycles/:cycleId/auction/... — GET, /schedule, /live-decision, /start, /pause, /resume, /end, /winner, /winner/manual, /winner/disqualify, /prebid/disqualify-and-declare-next) were deleted entirely; the auction-scoped staff routes in §3 replace them.
  • Removed response fields: the four decision booleans (canDirectDeclareFromPrebid, canRunPrebidLot, canProceedToLiveAuction, liveBiddingDecisionPending), reviewStatus, liveStartMode, and availabilityPhase no longer exist.
  • No implicit manual start: the Start command requires the prebid window closed, and (when approvalRequired) a prior approval — drive the Start button off auctionStatus + approvalRequired, not a review status.
  • Leading bid is live-bid-only: the auction opens with leadingBid: null; a prebid never seeds the opening/base amount.
  • Staff prebid visibility: prebid amounts unlock for staff at ENDED (not at window close); subscribers unlock only after reveal.

10. Complete endpoint quick reference

10.1 Staff REST

For every row below, replace {staff} with companies for company scope or admin for superadmin scope unless a path is shown explicitly.

Method Path Purpose
GET /v2/{staff}/auctions List/filter auctions.
GET /v2/{staff}/auctions/:auctionId Authoritative console overview.
PATCH /v2/{staff}/auctions/:auctionId/schedule Schedule/reschedule and apply settings.
POST /v2/{staff}/auctions/:auctionId/approve Pass the approval gate and enter READY.
POST /v2/{staff}/auctions/:auctionId/cancel Cancel an auction.
POST /v2/{staff}/auctions/:auctionId/prebid/open Manually materialize the prebid window.
POST /v2/{staff}/auctions/:auctionId/prebid/close Manually close the prebid window.
POST /v2/{staff}/auctions/:auctionId/prebids/reveal Reveal sealed amounts after end.
DELETE /v2/{staff}/auctions/:auctionId/prebids/:prebidId Delete a prebid.
POST /v2/{staff}/auctions/:auctionId/winner/disqualify Disqualify a declared winner.
PATCH /v2/{staff}/auctions/:auctionId/trigger-policy Override automation for this auction.
GET /v2/{staff}/auctions/:auctionId/eligible-winner-candidates Populate the manual winner selector.
GET /v2/{staff}/auctions/:auctionId/action-logs Fetch the lifecycle audit trail.
GET /v2/{staff}/auctions/:auctionId/audience-sessions Fetch paginated sessions and participation audit.

The related policy/preset/program-settings endpoints are listed in §3.15–3.17. The staff detail endpoint accepts audienceFilter: ALL_ELIGIBLE, CURRENT_VIEWERS, ONLINE_PARTICIPANTS, OFFLINE_PARTICIPANTS, BIDDERS, FLOOR_PARTICIPANTS, or ELIGIBLE_NEVER_VIEWED. Live start/pause/resume/end, bid moderation, winner declaration/lot, announcements, floor presence, and floor bids use WebSocket commands rather than REST.

10.2 Subscriber REST

Method Path Purpose
GET /v2/subscribers/me/auctions Search/list subscriber auctions.
GET /v3/subscribers/me/auctions Mobile auction list.
GET /v2/subscribers/me/auctions/:auctionId Subscriber auction detail.
POST /v2/subscribers/me/auctions/:auctionId/prebid Place a prebid.
PUT /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId Edit a mutable prebid.
DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId Cancel a prebid.
GET /v2/subscribers/me/cycles/:cycleId/auction/:enrolledSubscriberId Auction-room bootstrap payload.
GET /v2/subscribers/me/cycles/:cycleId/overview/:enrolledSubscriberId Web cycle overview.
GET /v3/subscribers/me/cycles/:cycleId/overview/:enrolledSubscriberId Mobile cycle overview.

Live bids use auction.bid.place; there is no REST live-bid endpoint.

10.3 WebSocket commands

Command Caller Completion model
auction.subscribe subscriber/staff Immediate ack, then connected/snapshot/presence.
auction.unsubscribe subscriber/staff Immediate ack.
auction.participation.join subscriber Synchronous idempotent accepted/replayed result.
auction.bid.place subscriber/staff floor Synchronous terminal ack or correlated error.
auction.status.update staff Async command acknowledgement, then event/failure.
auction.pause staff Async command acknowledgement, then event/failure.
auction.resume staff Async command acknowledgement, then event/failure.
auction.end staff Async command acknowledgement, then event/failure.
auction.bid.mark staff Async command acknowledgement, then event/failure.
auction.winner.declare staff Async command acknowledgement, then event/failure.
auction.winner.record_lot staff Async command acknowledgement, then event/failure.
auction.announcement.create staff Async command acknowledgement, then event/failure.
auction.presence.floor.update staff Async command acknowledgement, then auction.audience.changed.

auction.bid.cancel is not implemented. Do not expose it in the frontend.


11. End-to-end frontend flows

11.1 Open an auction room

  1. Fetch the role-appropriate REST detail and render a loading-safe snapshot.
  2. Open /ws with websocket.v1, bearer.<access-token>.
  3. When the socket is open, send auction.subscribe with a unique request ID, auctionId, subscriber enrollment when applicable, and the last persisted sequence when reconnecting.
  4. Expect the subscribe ack, then auction.connected. Apply auction.state.snapshot as replacement aggregate state and auction.audience.snapshot as replacement presence.
  5. Apply subsequent canonical events in sequence.

11.2 Place a live bid

  1. Enable the form only when the snapshot says LIVE and currentUser.canBid is true. If currentUser.canJoin is true, send auction.participation.join first with a stable request ID. Joining is optional and does not submit a bid. Still expect the server to reject stale UI state.
  2. Parse the amount using decimal-string logic and produce positive amountMinor.
  3. Create and persist a stable request ID with the frozen payload.
  4. Send auction.bid.place and show that intent as pending.
  5. On matching ack with accepted or replayed, mark it committed and use bidId/stateVersion for correlation.
  6. On matching error, show its safe message and mark the intent rejected.
  7. On disconnect/timeout, mark it uncertain and retry the unchanged ID/payload after reconnect; do not create a replacement ID.
  8. Independently apply auction.bid.created, auction.bid.updated, and extended events to shared room state.

11.3 Staff lifecycle control

  1. Render buttons from §8 using the latest overview/snapshot.
  2. Give each staff command a request ID and show an operation-level pending state after command.acknowledged.
  3. Resolve that pending state on the corresponding canonical event or a correlated command.rejected/command.failed.
  4. Refetch the overview after winner, policy, schedule, or visibility changes because these affect multiple projections.
  5. Never locally force the next status when the countdown reaches zero; wait for the lifecycle event or reconcile with REST.

11.4 Recover from an event gap

  1. Detect transport loss, malformed replay, or state that cannot be reconciled. Do not treat a numeric sequence gap alone as loss: scoped events make the visible stream intentionally sparse.
  2. Freeze delta application and keep the screen visibly reconnecting.
  3. Re-subscribe with the last applied high-water lastSequence.
  4. Replace aggregate state from the snapshot, reconcile the existing feed with replay in order as described in §5.5, then resume live deltas.
  5. If convergence cannot be proven, perform the role-appropriate REST detail fetch and repeat the subscription.

12. Release checklist for frontend teams

  • Remove expectedAuctionVersion from bid payloads and reject it in local request builders/tests.
  • Generate stable bid IDs, persist uncertain intents, and support accepted/replayed as equal success outcomes.
  • Keep currency conversion integer/string based.
  • Distinguish synchronous bid ack from asynchronous staff command.acknowledged.
  • Replace state on snapshots and deduplicate/order deltas by serverSequence.
  • Separate view, join, and bid UI states. Do not require payment to watch.
  • Handle T−10 open/close events without refreshing and persist the join command ID across uncertain retries.
  • Never render subscriber audience identities; use the four aggregate counts.
  • Treat stateVersion as server output, never as bid concurrency input.
  • Handle every status, prebidPhase, currentPhase, and transition listed in this document, including extended, extension_started, and call_started.
  • Treat floor-backed subscriber sockets as viewing-only; support the confirmation_required join ack and explicit floor-to-online confirmation.
  • Restore regularAuctionEndAt and extensionStartedAt alongside auctionEndAt from REST and lifecycle snapshots.
  • Hide all removed review/live-decision/candidate-disqualification UI.
  • Do not expose nextTransitionAt, scheduler names, or repair controls.
  • Test 5 and 10 concurrent bid submissions and assert every request reaches a committed, replayed, rejected, or explicitly uncertain UI state.
  • Test disconnect-after-send followed by retry with the same ID.
  • Test automatic open/start/end/reveal/winner transitions at their persisted deadlines and verify separate immediate-chain events.
  • Test background-tab resume and sequence-gap recovery.
  • Verify prebid amount redaction for staff and subscribers against §6.