Manual Prebid Controls — Frontend Integration Guide¶
This guide is the complete, frontend-facing contract for manually opening and closing the prebid window from the company console and the superadmin console, plus the new rule that the live auction cannot start while prebid is open.
Both consoles share exactly the same contract. The only differences are the REST mount (/v2/companies vs /v2/admin) and the role token used. Manual prebid controls are REST-only — unlike the live-auction controls (start / pause / resume / end), which are WebSocket staff commands.
Audience: frontend engineers integrating the auction console.
Related docs:
1. TL;DR¶
| Action | Transport | Endpoint | Success event | Action log |
|---|---|---|---|---|
| Open prebid | REST | POST /v2/companies\|admin/auctions/:auctionId/prebid/open | auction.state.changed transition: "prebid_opened" | MANUAL_OPEN_PREBID |
| Close prebid | REST | POST /v2/companies\|admin/auctions/:auctionId/prebid/close | auction.state.changed transition: "prebid_closed" | MANUAL_CLOSE_PREBID |
| Read state | REST | GET /v2/companies\|admin/auctions/:auctionId | console overview payload | — |
| Audit | REST | GET /v2/companies\|admin/auctions/:auctionId/action-logs | action log list | — |
Roles: COMPANY and SUPERADMIN. Company tokens are scoped to auctions owned by their company; superadmin tokens can act on any auction.
The rule that matters most: the live auction may only start once the prebid window is closed. Any manual start while prebid is open is rejected server-side with BAD_REQUEST ("Close the prebid window before starting the live auction."). Drive your Start button off the closed state, not just the scheduled status.
2. Where manual controls fit in the prebid lifecycle¶
The prebid window can open and close in two ways:
| Path | Trigger | prebidPhase |
|---|---|---|
| Automatic (policy) | autoOpenPrebid opens the window when the auction is armed; it closes by its deadline (prebidEndAt or end of the cycle day) | OPEN → CLOSED |
| Manual (staff) | POST …/prebid/open and POST …/prebid/close | same phases, driven by staff action |
Phase flow around the controls:
OPEN ──(manual close OR deadline passes)──▶ CLOSED
▲ │
│ │ start path (see §6)
└───────── (reopen is NOT allowed) ▼
approve (only when approvalRequired)
│
▼
Start (manual START or auto-start)
Key semantics:
- Open materializes/records the window explicitly (
prebidStartAt= now,prebidEndAt= existing deadline orendOfDay(cycleEndDate)). It is only accepted while the window is still naturally open (prebidPhase === 'OPEN'); a window that has already closed cannot be reopened. - Close pins
prebidEndAtto now, immediately blocking new subscriber prebid submissions. Existing prebids keep theirprebidMutabilityPolicybehavior (e.g.IMMUTABLE_CANCEL_ONLYstill allows cancel-only, no edits). - Both actions are idempotent: repeating an open on an already-open window, or a close on an already-closed window, returns
200with theprebidOpened/prebidClosedflag set tofalse. - A manual close also clears any pending automatic open job, so the auto-policy cannot re-open the window afterwards.
3. Endpoints and payloads¶
3.1 Open the prebid window¶
Request body — reason is optional:
Successful 200 response (data):
{
"auctionId": "auc_01H…",
"cycleId": "cyc_01H…",
"stateVersion": 9,
"prebidStartAt": "2026-08-17T09:00:00.000Z",
"prebidEndAt": "2026-08-19T23:59:59.999Z",
"prebidOpened": true
}
| Field | Type | Description |
|---|---|---|
auctionId | string | Auction id. |
cycleId | string | Cycle id. |
stateVersion | number | New auction state version (incremented on a real open). |
prebidStartAt | string | Window open time (now on first open, existing value on no-op). |
prebidEndAt | string | Window deadline (existing or endOfDay(cycleEndDate)). |
prebidOpened | boolean | true when the window was opened by this call, false if it was already open (idempotent no-op). |
3.2 Close the prebid window¶
Request body — reason is optional:
Successful 200 response (data):
{
"auctionId": "auc_01H…",
"cycleId": "cyc_01H…",
"stateVersion": 10,
"prebidEndAt": "2026-08-17T09:30:00.000Z",
"prebidClosed": true
}
| Field | Type | Description |
|---|---|---|
auctionId | string | Auction id. |
cycleId | string | Cycle id. |
stateVersion | number | New auction state version (incremented on a real close). |
prebidEndAt | string | Pinned to the close time on first close; existing value on no-op. |
prebidClosed | boolean | true when the window was closed by this call, false if it was already closed (idempotent no-op). |
prebidStartAt | (absent) | Not returned by the close endpoint. |
3.3 Response envelope¶
Both endpoints use the standard success envelope:
("Prebid window closed successfully." for close.) Errors use the standard { status: "error", code, message } envelope with requestId.
4. Validation and business rules¶
Applies to both open and close (unless noted):
| Rule | Failure (400) |
|---|---|
| Program bid type is auction-based | "Auction is not enabled for this program." |
Cycle is ACTIVE | "Auction operations are only allowed for active cycles." |
| Prebid is enabled for the auction | "Prebid is not enabled for this auction." |
Auction is SCHEDULED | "Prebid can only be opened/closed while the auction is scheduled." |
| Company owns the auction (company token) | 403 "You do not have access to this auction." |
Window is still naturally open (prebidPhase === 'OPEN') | close: idempotent no-op; open: 400 "Prebid window has already closed and cannot be reopened manually." |
A deadline is derivable (cycleEndDate or prebidEndAt) | open only: 400 "Prebid window cannot be opened without a cycle end date." |
Window not already open (prebidStartAt unset) | open only: idempotent no-op (prebidOpened: false). |
| Window not already closed by time | close only: idempotent no-op (prebidClosed: false). |
Actions that become available after a close:
prebidPhase→CLOSED(derived; there is no prebid review state anymore — the sealed prebids are not touched until the live auction ends).- The start path opens: when
approvalRequired === true,POST .../approvemoves the auction toREADY, then Start; whenapprovalRequired === false(default) no approval is needed and the auction starts directly fromSCHEDULED— see section 6.
5. Reading state and auditing¶
Poll or reconcile the console after each mutation:
GET /v2/companies/auctions/:auctionId (COMPANY token)
GET /v2/admin/auctions/:auctionId (SUPERADMIN token)
Relevant fields in the overview payload:
| Field | Meaning |
|---|---|
prebidPhase | OPEN → CLOSED after a close (derived from auctionStatus + window state). |
prebidStartAt | Set after a manual (or auto) open; null until then. |
prebidEndAt | Original deadline; pinned to the close moment after a manual close. |
activePrebidCount | Number of active prebids received before the window closed. |
stateVersion | Bumped on every open/close. |
Audit trail — both actions are recorded:
action | actorId | metadata |
|---|---|---|
MANUAL_OPEN_PREBID | staff id | prebidStartAt, prebidEndAt (ISO-8601) |
MANUAL_CLOSE_PREBID | staff id | prebidEndAt (ISO-8601) |
6. The live start gate¶
The live auction can only start once the prebid window is closed. This is enforced server-side in three layers (state machine, guard helper, and the start command):
| Attempt | Result |
|---|---|
Manual start (WS auction.status.update START) while prebidPhase === 'OPEN' | error frame, code: "BAD_REQUEST", message "Close the prebid window before starting the live auction." |
Manual start while prebidPhase === 'CLOSED' but approvalRequired and auction not READY | error frame, code: "BAD_REQUEST", message "Approve this auction before starting the live auction." |
Manual start from auctionStatus === 'READY' (approved) | Allowed. |
Manual start while prebidPhase === 'CLOSED' and approvalRequired === false (default) | Allowed — the auction starts directly from SCHEDULED. |
| Auto-scheduled start | Same gate: only fires once prebidPhase === 'CLOSED' (and the auction is READY when approvalRequired). |
Recommended console button states¶
| Button | Visible / enabled when |
|---|---|
| Open prebid | auctionStatus === 'SCHEDULED', settings.hasPrebid === true, prebidPhase === 'OPEN', and prebidStartAt is null. |
| Close prebid | auctionStatus === 'SCHEDULED', settings.hasPrebid === true, prebidPhase === 'OPEN'. |
| Start (live) | prebidPhase === 'CLOSED' and (approvalRequired === false or auctionStatus === 'READY'). Never show Start while prebid is open. |
This replaces the previous behavior where Start was offered while prebid was still open. Keep the Close button prominent between "prebid open" and "prebid closed" so operators close the window before starting.
7. Realtime events¶
Both mutations publish an auction.state.changed event to the auction:<auctionId> room (staff and subscribers). The example abbreviates the complete payload.state to the prebid fields used by this screen:
{
"type": "auction.state.changed",
"id": "evt_01H…",
"version": "1.0",
"source": "auction-service",
"timestamp": "2026-08-17T09:30:00.000Z",
"sequence": 12,
"data": {
"auctionId": "auc_01H…",
"cycleId": "cyc_01H…",
"stateVersion": 10,
"payload": {
"transition": "prebid_closed",
"state": {
"status": "SCHEDULED",
"stateVersion": 10,
"currentPhase": "PREBID_CLOSED",
"prebidPhase": "CLOSED",
"prebidStartAt": "2026-08-17T08:00:00.000Z",
"prebidEndAt": "2026-08-17T09:30:00.000Z"
}
}
}
}
payload.transition | Fired by | Frontend handling |
|---|---|---|
prebid_opened | Manual open (and auto-policy open) | Replace lifecycle state, start the prebid countdown, enable prebid submission UI. |
prebid_closed | Manual close | Replace lifecycle state, stop submissions, and enable the Start path. |
Subscriber impact of prebid_closed: their REST prebid calls are rejected ("Prebid window is not open.") and their currentUser.canPrebid flips to false on the next snapshot/detail fetch. Render the form as locked on the event, do not wait for a poll.
Note: subscribers who are not subscribed to the room still see the change the next time they load the auction — the REST overview is always the source of truth for cold starts.
8. Sequence diagram¶
sequenceDiagram
participant Staff as Staff console
participant API as REST API
participant Svc as Auction service
participant Room as Auction room (WS)
participant Sub as Subscriber app
Note over Staff,Svc: Open (optional — records the window early)
Staff->>API: POST .../auctions/:id/prebid/open {reason}
API->>Svc: openPrebid command
Svc-->>API: 200 {prebidStartAt, prebidEndAt, prebidOpened: true}
Svc-->>Room: auction.state.changed {transition: prebid_opened}
Note over Staff,Svc: Close — required before the live auction starts
Staff->>API: POST .../auctions/:id/prebid/close {reason}
API->>Svc: closePrebid command
Svc->>Svc: prebidEndAt = now, clear auto-open job
Svc-->>API: 200 {prebidEndAt, prebidClosed: true}
Svc-->>Room: auction.state.changed {transition: prebid_closed}
Room-->>Sub: canPrebid = false, form locked
Note over Staff,Svc: Approve (only when approvalRequired; else start directly)
Staff->>API: POST .../auctions/:id/approve {reason}
API->>Svc: approveAuction command
Svc-->>API: 200 {auctionStatus: READY}
Note over Staff,Svc: Live start is now possible
Staff->>Svc: auction.status.update {status: START}
Svc-->>Room: auction.state.changed {transition: started} 9. Recommended client flow¶
1. Fetch GET .../auctions/:auctionId → seed prebidStartAt / prebidEndAt /
prebidPhase / auctionStatus / settings.hasPrebid / approvalRequired.
2. Subscribe to the auction room (staff: auction.subscribe with auctionId (fetch via REST)).
3. Render Open/Close buttons per the visibility table in section 6.
4. On Open tap → POST .../prebid/open → update prebidStartAt / prebidEndAt.
5. On Close tap → POST .../prebid/close → set prebidEndAt = now, prebidPhase to
CLOSED, enable Start.
6. Reconcile with auction.state.changed (prebid_opened / prebid_closed) so all
consoles stay in sync.
7. On reconnect → resubscribe with lastSequence, re-fetch the overview.
Event handling sketch¶
function handleFrame(frame: any) {
if (frame.type === 'auction.state.changed') {
const t = frame.data?.payload?.transition;
if (t === 'prebid_opened') {
setPrebidWindow({ open: true });
}
if (t === 'prebid_closed') {
setPrebidWindow({ open: false });
setPrebidPhase('CLOSED');
}
if (t === 'started') setStatus('LIVE');
}
if (frame.type === 'error') {
// code in { BAD_REQUEST, FORBIDDEN, NOT_FOUND, CONFLICT, INTERNAL }
showToast(frame.data.message);
}
}
10. Error reference¶
| Code | Message (abridged) | When |
|---|---|---|
404 | Auction not found | Unknown auction id. |
403 | You do not have access to this auction | Company token on another company's auction. |
400 | Auction is not enabled for this program | Program bid type is not auction-based. |
400 | Auction operations are only allowed for active cycles | Cycle not ACTIVE. |
400 | Prebid is not enabled for this auction | settings.hasPrebid === false. |
400 | Prebid can only be opened while the auction is scheduled | Auction already LIVE/PAUSED/ENDED. |
400 | Prebid can only be closed while the auction is scheduled | Same, close variant. |
400 | Prebid window has already closed and cannot be reopened manually | Open after the window closed by time. |
400 | Prebid window cannot be opened without a cycle end date | No cycleEndDate and no explicit prebidEndAt to derive a deadline from. |
200 no-op | prebidOpened: false / prebidClosed: false | Repeating an already-applied action (idempotent). |
500 | Server error | Unexpected failure. |