Skip to content

Subscriber Prebid — WebSocket Integration Guide

This document is the complete realtime contract for subscriber prebid in the auction flow, written for frontend teams integrating the Dichit auction room.

Prebid is split across two transports:

  • REST — all prebid actions (place, edit, cancel) are HTTP calls on /v2/subscribers/me/auctions/:auctionId/prebid… (see section 6).
  • WebSocket — the realtime stream on /ws pushes prebid lifecycle events and carries the full prebid state inside the auction snapshot.

The WebSocket does not create, update or cancel prebids. It tells the UI what changed while the REST API is the source of truth for the write result.

Reference: events/auction.md · events/presence.md · protocol.md · frontend-integration.md · errors.md · rate-limits.md


1. Prebid lifecycle overview

A cycle auction's prebid window is modelled by the derived prebidPhase on data.payload.auction (the persisted auctionStatus — PENDING/SCHEDULED/ READY/LIVE/PAUSED/ENDED/COMPLETE/CANCELLED — tracks live/end state, not the prebid window):

Phase (data.payload.auction.prebidPhase) Meaning
NONE No prebid phase (no prebid window is open).
OPEN Subscribers can submit/cancel prebid.
CLOSED Prebid window closed; submissions no longer allowed.

Key rules that gate prebid for a subscriber (all enforced server-side):

  1. The program bid type is auction-based and hasPrebid is enabled.
  2. The cycle is active and the prebid phase is OPEN (now between prebidStartAt and prebidEndAt).
  3. The subscriber has completed payment for that exact cycle.
  4. The subscriber has not already won once in the program.
  5. The subscriber has no active PREBID-phase disqualification.
  6. Only one active prebid per subscriber per auction exists.
  7. MUTABLE policy → an existing active prebid is updated in place.
  8. IMMUTABLE_CANCEL_ONLY policy → update is rejected; the subscriber may only cancel (REST DELETE) and then submit a new one.
  9. If the program requires a prebid document, a valid signatureAssetId is required; the rendered PDF is generated asynchronously by the server.

Prebid status values: ACTIVE, CANCELLED, DISQUALIFIED, APPLIED, SUPERSEDED.


2. Connecting and authenticating

Single endpoint for every realtime role:

wss://api.example.com/ws

Browser clients pass the access token through Sec-WebSocket-Protocol:

Sec-WebSocket-Protocol: websocket.v1, bearer.<access-token>

Native/mobile clients may use the Authorization: Bearer <access-token> header. Invalid or missing tokens close the socket with code 1008.

Server heartbeat: protocol-level ping/pong plus the heartbeatIntervalMs reported in the auction.connected event. Send system.ping for application-level health checks.


3. Subscribe to the auction room

To receive prebid realtime state the subscriber subscribes to the auction room:

{
  "id": "subscribe-prebid-001",
  "type": "auction.subscribe",
  "version": "1.0",
  "source": "web-client",
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "enrolledSubscriberId": "enr_01HQ5BWYP1Y5RX0W5X0W5X"
  }
}

enrolledSubscriberId is required for subscriber sockets. Optional data.lastSequence (the last observed auction-level data.serverSequence) requests replay of missed events, merged into the snapshot payload.

The server then sends, in order:

  1. auction.connected — subscription confirmation + connectionId and heartbeatIntervalMs.
  2. auction.state.snapshot — the full subscriber-scoped auction state, including all prebid fields (section 4).
  3. auction.audience.snapshot — current participants.
  4. An ack envelope echoing eventType: "auction.subscribe" plus auctionId, cycleId, room.

Example ack:

{
  "id": "subscribe-prebid-001",
  "type": "ack",
  "version": "1.0",
  "source": "dichit-backend",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 1,
  "data": {
    "eventType": "auction.subscribe",
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "room": "auction:auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X"
  }
}

Leave the room with auction.unsubscribe (data.auctionId) when the screen is closed; this also removes the subscriber's online presence.


4. Prebid state inside auction.state.snapshot

The snapshot envelope:

{
  "type": "auction.state.snapshot",
  "id": "evt_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 2,
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "payload": {
      "auction": {},
      "currentUser": {},
      "viewerCount": 0,
      "joinedParticipantCount": 0,
      "onlineParticipantCount": 0,
      "bidderCount": 0,
      "recentBids": [],
      "lastSequence": 0
    }
  }
}

data.payload.auction — prebid-relevant fields

Field Type Description
auctionId string Auction id.
cycleId string Cycle id.
prebidPhase string NONE | OPEN | CLOSED — the prebid phase from section 1.
prebidStartAt string Prebid window open time (ISO-8601).
prebidEndAt string Prebid window close time (ISO-8601).
auctionStartAt string Scheduled live auction start (ISO-8601).
auctionDurationSeconds number Live auction duration.
activePrebidCount number Number of active prebids (0 outside LIVE/PAUSED/ENDED).
minimumBidAmount number Floor for the bid amount.
totalAmount number Cycle total amount.
minimumBidReachedAt string When the minimum bid was reached (present after review).
resolutionType string How the auction resolves (e.g. direct/lot/live).
prebidAmountsRevealed boolean True after staff reveals completed-auction prebid amounts.
prebidAmountsRevealedAt string | null Reveal timestamp, or null before reveal.
settings object hasPrebid, prebidMutabilityPolicy, auctionBidMode, ...
bids array Visible bids; prebid entries are hidden until reveal.

data.payload.currentUser — the subscriber's own prebid state

Field Type Description
subscriberId string User id.
enrolledSubscriberId string Enrollment id used for this cycle.
isLeadingBidder boolean True if the subscriber currently leads the auction.
isPaymentCompletedForCycle boolean Prebid gate: payment done for the cycle.
hasAlreadyWonProgram boolean Prebid gate: already won once in this program.
canPrebid boolean True when the cycle is active, the auction accepts prebids, the phase is OPEN, payment is complete, the subscriber has not won, there is no PREBID disqualification, and no active prebid exists. It does not validate a submitted amount or signature asset.
prebidBlockedReason string | null Stable reason when canPrebid is false.
isPrebidDisqualified boolean PREBID-specific disqualification state.
isLiveBidDisqualified boolean LIVE-specific disqualification state; it does not independently block prebid.
activePrebid object | null Current ACTIVE prebid (see shape below).
latestPrebid object | null Most recent prebid regardless of status, incl. soft-deleted/disqualified (see shape below).
latestBid object | null Latest live bid (present once bidding started).
disqualifications array Bidder disqualifications (phase: "PREBID" blocks prebid).

activePrebid / latestPrebid shape:

Field Type Description
id string Prebid id (pb_…).
amount number Own prebid amount.
status string ACTIVE/CANCELLED/DISQUALIFIED/APPLIED/SUPERSEDED.
documentSubmissionId string | null Generated-document submission id (if required).
documentSubmissionStatus string | null PENDING/QUEUED/GENERATED/…
generatedPdfUrl string | null Rendered prebid document PDF URL.
disqualifiedAt string | null Disqualification time.
disqualificationReasonCode string | null KYC/payment/eligibility/rule reason.
deletionReason string | null Soft-delete reason.
deletedAt string | null Soft-delete time.
createdAt / updatedAt string Timestamps.

Frontend rule: render the prebid screen from currentUser.canPrebid, currentUser.prebidBlockedReason, currentUser.activePrebid and data.payload.auction.prebidPhase. Never let the UI decide eligibility — always trust the server fields.


All server events use the standard envelope; auction domain events carry data.auctionId, data.cycleId, optional data.stateVersion, optional data.serverSequence, and data.payload.

5.1 auction.prebid.created

Published whenever a subscriber creates a new prebid. This event is durable, staff-scoped, and delivered through auction:staff:<auctionId>; subscriber sockets do not receive it. Updating an existing active prebid does not emit it.

Payload field Type Description
payload.id string New prebid id.
payload.subscriberId string User id of the prebidder.
payload.enrolledSubscriberId string Enrollment id of the prebidder.
payload.bidderName string Staff display name.
payload.amount number | null null while sealed.
payload.createdAt string ISO-8601 creation time.
payload.status string AuctionPrebidStatus value.
payload.documentSubmissionId string | null Associated submission when a document exists.
{
  "type": "auction.prebid.created",
  "id": "evt_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-05T10:00:00.000Z",
  "sequence": 7,
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "serverSequence": 21,
    "payload": {
      "id": "pb_01HQ5BY3J7Z2V6Y0W5X0W5X0W5X",
      "subscriberId": "sub_01HQ5BWYP1Y5RX0W5X0W5X0W5X",
      "enrolledSubscriberId": "enr_01HQ5BWYP1Y5RX0W5X0W5X0W5X",
      "bidderName": "Annie Mohan",
      "amount": null,
      "createdAt": "2026-08-05T10:00:00.000Z",
      "status": "ACTIVE",
      "documentSubmissionId": null
    }
  }
}

The event is persisted in the auction activity replay log and carries data.serverSequence. Staff replay includes it; subscriber replay filters it out. amount remains nullable and is redacted while sealed.

Frontend handling: staff consoles may append/reconcile the prebid row using the payload and sequence. Subscriber screens must use the successful REST response plus snapshot/detail data for their own prebid state.

5.2 auction.prebid.deleted

Published by the staff/admin prebid delete path (DELETE /v2/admin/…/auctions/…/prebids/:prebidId or the auction.bid.mark/winner workflows that soft-delete a prebid). It carries targetUserId and is therefore delivered only to the affected subscriber (plus staff who see all events); other subscribers do not receive it.

Because the publish carries no payload, the socket adapter forwards the generic activity fallback, and fields with no value are omitted from the serialized frame (they are not sent as null):

Field Type Description
data.payload.metadata.prebidId string The deleted prebid id.
data.payload.metadata.enrolledSubscriberId string Enrollment id of the affected subscriber.
data.payload.bidId / data.payload.leadingBidAmount — Absent (undefined values are dropped on serialization).
{
  "type": "auction.prebid.deleted",
  "id": "evt_01HQ5BY3Y7Z2V6Y0W5X0W5X0W5X",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-05T10:05:00.000Z",
  "sequence": 9,
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "stateVersion": 4,
    "payload": {
      "metadata": {
        "prebidId": "pb_01HQ5BY3J7Z2V6Y0W5X0W5X0W5X",
        "enrolledSubscriberId": "enr_01HQ5BWYP1Y5RX0W5X0W5X0W5X"
      }
    }
  }
}

Frontend handling: on metadata.prebidId matching the locally held active prebid, clear currentUser.activePrebid / the prebid form, and re-evaluate canPrebid (re-fetch the snapshot or the auction detail for exactness).

Note: the subscriber's own REST cancel (DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId?enrolledSubscriberId=…) does not publish a realtime event — the REST response (status: "CANCELLED") is the source of truth for that action.

5.3 auction.state.changed — prebid-window transitions

Schedule changes (e.g. the prebid window opening/closing, or the auction going live) are delivered as auction.state.changed with data.payload.transition. The ones that matter for prebid:

transition When it fires Payload extras
schedule_changed Prebid window opened/closed or schedule updated prebidStartAt, prebidEndAt…
started Auction went live — prebid window is over status, auctionEndAt
winner_selected A winner was declared (possibly from a prebid) bidId/leadingBidAmount
prebids_revealed Completed-auction prebid amounts were revealed prebidAmountsRevealed
{
  "type": "auction.state.changed",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-05T09:00:00.000Z",
  "sequence": 3,
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "stateVersion": 2,
    "payload": {
      "transition": "schedule_changed",
      "prebidStartAt": "2026-08-05T09:00:00.000Z",
      "prebidEndAt": "2026-08-06T09:00:00.000Z"
    }
  }
}

Frontend handling: recompute the countdown, the submit/cancel button state, and switch the screen between prebid and live modes off data.payload.auction.prebidPhase (re-fetch auction.state.snapshot when needed).

5.4 auction.bidder.disqualified (prebid phase)

If a subscriber is disqualified from prebidding (phase: "PREBID"), the room receives auction.bidder.disqualified; the affected subscriber should treat an in-flight or submitted prebid as void and re-render the form as locked.

{
  "type": "auction.bidder.disqualified",
  "version": "1.0",
  "source": "auction-service",
  "timestamp": "2026-08-05T10:05:00.000Z",
  "sequence": 10,
  "data": {
    "auctionId": "auc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "cycleId": "cyc_01HQ5BWNJ5Y1P5RX0W5X0W5X0W5X",
    "stateVersion": 5,
    "payload": {
      "subscriberId": "enr_01HQ5BWYP1Y5RX0W5X0W5X0W5X",
      "reason": "KYC_ISSUE"
    }
  }
}

6. REST endpoints (the write path)

The WebSocket only streams state; prebid writes go through REST. All endpoints are scoped to the authenticated SUBSCRIBER and require :auctionId plus the enrolledSubscriberId of the enrollment being acted on (body for POST/PUT, query for DELETE).

Method Path Purpose
POST /v2/subscribers/me/auctions/:auctionId/prebid Place a new active prebid for the enrollment.
PUT /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId Edit the active prebid (MUTABLE_UNTIL_CLOSE only).
DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId?enrolledSubscriberId=… Cancel the active prebid (both policies).

Prebid state is read from the auction detail response (GET /v2/subscribers/me/auctions/:auctionId) — latestPrebid and enrollments[].prebids[] — there is no dedicated prebid GET endpoint.

POST / PUT body

Field Type Required Description
enrolledSubscriberId string Yes Enrollment the prebid belongs to.
amount number Yes Prebid amount (decimal accepted).
signatureAssetId string No Required only when the program demands a prebid document.

POST / PUT / DELETE response (data)

Field Type Description
auctionId string Auction id.
cycleId string Cycle id.
prebidId string Created/updated/cancelled prebid id.
enrolledSubscriberId string Enrollment the prebid belongs to.
status string ACTIVE (POST/PUT) / CANCELLED (DELETE).
documentSubmissionId string | null Document submission id, when a prebid document is generated.

Business-rule errors (HTTP 400 / 403 / 404)

Message (abridged) Meaning
Prebid window not open / auction started Place/edit/cancel outside the OPEN prebid phase.
You already have an active prebid for this auction An active prebid exists for this enrollment.
Payment not completed for cycle Cycle invoice unpaid.
Already won in this program One-win-per-program rule.
Prebid cannot be updated after submission IMMUTABLE_CANCEL_ONLY policy.
Active prebid not found Prebid id unknown or not owned by the subscriber/enrollment.
A signature is required / template missing Prebid document not satisfiable.
Bid amount outside allowed range Amount validation (minimumBidAmount…).

  1. Open /ws with the access token.
  2. auction.subscribe with auctionId (fetch via REST; auction row guaranteed before READY/LIVE) + enrolledSubscriberId (and lastSequence when reconnecting).
  3. On auction.state.snapshot:
  4. Render phase from data.payload.auction.prebidPhase.
  5. Render the prebid form/banner from data.payload.currentUser.canPrebid, activePrebid, latestPrebid, and the document gates.
  6. Show activePrebidCount from data.payload.auction.activePrebidCount.
  7. When the user submits: call REST POST /v2/subscribers/me/auctions/:auctionId/prebid with enrolledSubscriberId + amount.
  8. Update the UI from the REST response (prebidId, status).
  9. Do not await auction.prebid.created; it is staff-only. Re-read the auction detail response when reconciliation is required.
  10. When the user cancels: call REST DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId?enrolledSubscriberId=… (allowed under both mutability policies). The REST response is the source of truth; no event is pushed.
  11. Apply auction.state.changed transitions to switch prebid ⇄ live modes and update countdowns.
  12. Handle auction.bidder.disqualified (prebid phase) by locking the form.
  13. auction.unsubscribe when leaving the screen.
  14. On reconnect: resubscribe with lastSequence, rebuild from the fresh snapshot, then apply pushed events.

8. Sequencing and reconnection

  • data.serverSequence on bid-activity events (auction.bid.created, auction.bid.updated, …) is the auction-level cursor. Persist it and pass it back as lastSequence on resubscribe to replay anything missed. Prebid events are not persisted in the replay log and carry no serverSequence; after a gap, re-read own prebid state from the auction detail response or the fresh snapshot.
  • The envelope-level sequence is per-connection only; a gap means dropped frames → resubscribe with lastSequence.
  • Replay events are merged into the snapshot payload as data.payload.replay when lastSequence is provided.

9. Rate limits

Scope Limit
Default (incl. subscribe) 120 messages / 60 s
auction.bid.place 100 messages / 60 s

Exceeding a limit returns an error envelope with code: "RATE_LIMITED".