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 betweenSCHEDULEDandLIVE. - 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.placeresponse 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 thelive-decisionendpoints are removed.
This page is the full contract. Companion deep dives (linked inline) cover individual areas in more detail.
Related docs:
- Auction — general integration guide
- Strict auction lifecycle — frontend contract
- Auction lifecycle control (start / pause / resume / end)
- Manual prebid controls (open / close)
- Terminal live-bid responses
- Subscriber auction detail (REST + WebSocket)
- Canonical auction WebSocket events
- Winner declaration (backend)
- Auction & prebid (backend contract)
- Starting an auction (backend flow)
- Auction lifecycle config (settings & policy)
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¶
- Bootstrap the screen from REST, then subscribe to the auction room.
- Keep the highest applied
serverSequenceand the lateststateVersion. - Generate one stable request
idfor each bid intent. - Treat the correlated bid
ackorerroras terminal. If delivery is uncertain, retry the identical bid with the identicalid. - Re-subscribe with
lastSequenceafter reconnect, replace aggregate state from the snapshot, and reconcile the activity feed from its replay parcel. - Use server timestamps for countdown display only. The server, not the browser clock, decides whether an action is still legal.
1. The three rules¶
- No winner selection before the live auction ends. Winner actions (
auction.winner.declare,auction.winner.record_lot) are rejected while the auction isSCHEDULED/READY. Winner selection happens only afterauctionStatus === ENDED. - 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. - 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¶
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¶
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¶
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)¶
Body (optional): { "reason": "Proceeding to live round" }
- When:
auctionStatus === SCHEDULEDand resolvedapprovalRequiredistrue. - Effect:
auctionStatus→READY; lifecycle re-armed; action logAPPROVEwritten withfromStatus/toStatus/approvalRequiredmetadata. - 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."400Illegal transition if notSCHEDULED.- Approval publishes a durable
auction.state.changedevent withpayload.transition: "approved". Replace lifecycle state frompayload.state; the REST response remains the terminal result for the initiating request.
3.5 Cancel auction¶
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;400if the window already closed ("Prebid window has already closed and cannot be reopened manually."). - close: pins
prebidEndAt = now, immediately blocking new prebid submissions. Idempotent.prebidPhasederives toCLOSED. - Response
dataincludesprebidStartAt/prebidEndAtand booleansprebidOpened/prebidClosed(false when the action was an idempotent no-op). Publishesauction.state.changedwithtransition: "prebid_opened"/"prebid_closed". Action logsMANUAL_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 === ENDEDonly —400"Prebid amounts can be revealed only after the auction ends." otherwise. - Stamps
prebidAmountsRevealedAt, publishesauction.state.changed { transition: "prebids_revealed" }. Action logREVEAL_PREBIDS.
3.8 Action logs (audit trail)¶
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¶
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¶
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 validsignatureAssetIdis required.
Response data: { auctionId, cycleId, prebidId, enrolledSubscriberId, status: "ACTIVE", documentSubmissionId }.
4.4 Edit prebid¶
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¶
Query requires enrolledSubscriberId. Frees the slot for a new prebid while the window is open.
4.6 Cycle-scoped auction detail (v2)¶
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→ requiresbidId;reasonCodeoptional.DISQUALIFY_BIDDER→ requiresenrolledSubscriberId+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 scopedidand 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:
- Open a new authenticated socket.
- Send
auction.subscribewith the highest durably appliedlastSequenceand, for a subscriber, itsenrolledSubscriberId. - Replace aggregate auction state with
snapshot.data.payload.auction. - When replay is present, keep the feed that existed through the requested cursor, discard the snapshot's
recentBidstail, and apply visiblesnapshot.data.payload.replay.eventsin ascending sequence. The list may be sparse because staff-only and other-user-targeted events are filtered. - Set the cursor to
snapshot.data.payload.replay.lastSequence. On a fresh subscription without replay, build the feed fromrecentBidsand use the snapshot'slastSequence. - 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
expectedAuctionVersionchecks, and any UI that interprets an immediate acknowledgement as only "queued".auction.bid.placenow returnsack.data.status = accepted | replayedonly after the database outcome is known, or a correlatederror. - 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
nextTransitionAtfield and no frontend scheduler API. - Single status axis:
reviewStatus+availabilityPhase+liveStartModeare gone. Drive UI offauctionStatus(+ derivedprebidPhase+ resolvedapprovalRequired). For a single phase badge, use the new derivedcurrentPhaseon everyGET .../auctions/:auctionIdresponse (§2.4). availabilityPhase→prebidPhaseeverywhere 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 overviewbidOverview(v2 and v3), and the v3 auctions list.- Approval is a gate, not a mode: the
live-decisionendpoint (and itsAPPROVE_LIVE_AUCTIONaction log) is removed. UsePOST .../approve→READYwhenapprovalRequiredis enabled; otherwise start directly fromSCHEDULED. approvalRequiredis now a config key: added to the platform/company policy, auction-preset, program-settings, and program-creation (step 2) write bodies and totriggerPolicy/provenanceon every settings/policy read (see §3.16).- Removed endpoints:
POST .../prebid/disqualify-and-declare-nextand theDISQUALIFY_CANDIDATE_AND_DECLARE_NEXTauction.bid.markaction 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, andavailabilityPhaseno longer exist. - No implicit manual start: the Start command requires the prebid window closed, and (when
approvalRequired) a prior approval — drive the Start button offauctionStatus+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¶
- Fetch the role-appropriate REST detail and render a loading-safe snapshot.
- Open
/wswithwebsocket.v1, bearer.<access-token>. - When the socket is open, send
auction.subscribewith a unique request ID,auctionId, subscriber enrollment when applicable, and the last persisted sequence when reconnecting. - Expect the subscribe
ack, thenauction.connected. Applyauction.state.snapshotas replacement aggregate state andauction.audience.snapshotas replacement presence. - Apply subsequent canonical events in sequence.
11.2 Place a live bid¶
- Enable the form only when the snapshot says
LIVEandcurrentUser.canBidis true. IfcurrentUser.canJoinis true, sendauction.participation.joinfirst with a stable request ID. Joining is optional and does not submit a bid. Still expect the server to reject stale UI state. - Parse the amount using decimal-string logic and produce positive
amountMinor. - Create and persist a stable request ID with the frozen payload.
- Send
auction.bid.placeand show that intent as pending. - On matching
ackwithacceptedorreplayed, mark it committed and usebidId/stateVersionfor correlation. - On matching
error, show its safe message and mark the intent rejected. - On disconnect/timeout, mark it uncertain and retry the unchanged ID/payload after reconnect; do not create a replacement ID.
- Independently apply
auction.bid.created,auction.bid.updated, andextendedevents to shared room state.
11.3 Staff lifecycle control¶
- Render buttons from §8 using the latest overview/snapshot.
- Give each staff command a request ID and show an operation-level pending state after
command.acknowledged. - Resolve that pending state on the corresponding canonical event or a correlated
command.rejected/command.failed. - Refetch the overview after winner, policy, schedule, or visibility changes because these affect multiple projections.
- 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¶
- 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.
- Freeze delta application and keep the screen visibly reconnecting.
- Re-subscribe with the last applied high-water
lastSequence. - Replace aggregate state from the snapshot, reconcile the existing feed with replay in order as described in §5.5, then resume live deltas.
- If convergence cannot be proven, perform the role-appropriate REST detail fetch and repeat the subscription.
12. Release checklist for frontend teams¶
- Remove
expectedAuctionVersionfrom bid payloads and reject it in local request builders/tests. - Generate stable bid IDs, persist uncertain intents, and support
accepted/replayedas equal success outcomes. - Keep currency conversion integer/string based.
- Distinguish synchronous bid
ackfrom asynchronous staffcommand.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
stateVersionas server output, never as bid concurrency input. - Handle every status,
prebidPhase,currentPhase, and transition listed in this document, includingextended,extension_started, andcall_started. - Treat floor-backed subscriber sockets as viewing-only; support the
confirmation_requiredjoin ack and explicit floor-to-online confirmation. - Restore
regularAuctionEndAtandextensionStartedAtalongsideauctionEndAtfrom 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.