Subscriber Prebid Lifecycle API (v2)¶
Auction-keyed endpoints that let a subscriber place, edit, and cancel their own prebid for an auction. These are the only subscriber prebid endpoints — the legacy cycle-keyed variants (/v2/subscribers/me/cycles/:cycleId/prebid/…) have been removed in favor of this API.
Base URL¶
Authentication¶
All endpoints require a valid JWT bearer token for a SUBSCRIBER role user.
Business Rules¶
| # | Rule |
|---|---|
| 1 | A subscriber may hold at most one active prebid per enrollment per auction. Placing a new prebid while an active one exists for the same enrollment is rejected. |
| 2 | Every request must identify the enrollment it targets via enrolledSubscriberId, because a subscriber can have multiple enrollments in the same program. The enrollment is resolved server-side and must belong to the authenticated subscriber and the auction's cycle. |
| 3 | A subscriber may place a prebid only while the cycle is ACTIVE, the auction is PENDING, SCHEDULED, or READY, and the prebid phase is OPEN. Payment, prior-winner, PREBID-disqualification, document, and amount rules also apply. |
| 4 | A subscriber may edit a prebid only while the prebid window is open and the program's PrebidMutabilityPolicy is MUTABLE_UNTIL_CLOSE. Under IMMUTABLE_CANCEL_ONLY, edits are rejected. |
| 5 | A subscriber may cancel an active prebid only while the prebid window is open. Cancellation is allowed under both mutability policies. |
| 6 | After cancellation the prebid is CANCELLED and no longer blocks a new prebid — the subscriber can place a fresh one (for the same enrollment) while the window remains open. |
Lifecycle¶
place ──→ ACTIVE ──(cancel)──→ CANCELLED ──(place)──→ ACTIVE (new prebid)
│
└──(edit, only when MUTABLE_UNTIL_CLOSE)──→ ACTIVE (updated amount)
Rules 1–6 apply per enrolledSubscriberId: a subscriber with two enrollments in the same program holds up to one active prebid per enrollment and must pass the relevant enrolledSubscriberId on every call.
Statuses come from the AuctionPrebidStatus enum: ACTIVE, CANCELLED, DISQUALIFIED, APPLIED, SUPERSEDED.
The PrebidMutabilityPolicy enum: IMMUTABLE_CANCEL_ONLY, MUTABLE_UNTIL_CLOSE.
Fetch GET /v2/subscribers/me/auctions/:auctionId before rendering the form. For the selected enrollment, use currentUser.canPrebid and currentUser.prebidBlockedReason. Detail viewing does not require payment or live-auction participation. Placing a prebid requires payment but does not require joining the live auction.
Common Response Envelope¶
Every success response uses this shape:
{
"status": "success", // "success" | "error" | "pending"
"code": 200,
"message": "...",
"data": {/* endpoint-specific payload */},
"error": null
}
All three mutation endpoints return the same data shape:
{
"auctionId": "cuid2-string",
"cycleId": "cuid2-string",
"prebidId": "cuid2-string",
"enrolledSubscriberId": "cuid2-string", // enrollment the prebid belongs to
"status": "ACTIVE", // AuctionPrebidStatus
"documentSubmissionId": "cuid2-string" | null // PDF submission created for the prebid, if any
}
Place a Prebid¶
POST /v2/subscribers/me/auctions/:auctionId/prebid¶
Creates a new active prebid for the authenticated subscriber.
Attributes¶
| Field | Value |
|---|---|
| Method | POST |
| Path | /v2/subscribers/me/auctions/:auctionId/prebid |
| Auth | Bearer <access> + authorize(['SUBSCRIBER']) |
| Content-Type | application/json |
Path parameters¶
| Param | Type | Required | Description |
|---|---|---|---|
auctionId | cuid2 string | yes | ID of the auction to prebid for |
Body¶
| Field | Type | Required | Description |
|---|---|---|---|
enrolledSubscriberId | cuid2 string | yes | Enrollment this prebid is placed for; must belong to the subscriber and the auction's cycle |
amount | number | yes | Half-up normalized to two decimals, then validated against bid mode, minimum, and total |
signatureAssetId | cuid2 string | no | Signature asset ID when the program requires a prebid document |
Response 200¶
{
"status": "success",
"code": 200,
"message": "Prebid placed successfully.",
"data": {
"auctionId": "cuid2-string",
"cycleId": "cuid2-string",
"prebidId": "cuid2-string",
"status": "ACTIVE",
"documentSubmissionId": null
}
}
Errors¶
| Status | Message | When |
|---|---|---|
| 401 | Unauthorized | missing/invalid token |
| 403 | Forbidden | role is not SUBSCRIBER, not part of the cycle, or disqualified from the prebid |
| 400 | Invalid ID format / Value must be a valid decimal number | malformed auctionId / enrolledSubscriberId / signatureAssetId / amount, or missing enrolledSubscriberId / amount |
| 400 | Prebid is not enabled for this program. | program has hasPrebid false |
| 400 | Prebid window is not open. | prebid is closed or the auction is outside PENDING, SCHEDULED, and READY |
| 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 is not ACTIVE |
| 400 | You already have an active prebid for this auction. Cancel it before placing a new one. | an active prebid exists for this auction |
| 400 | Complete the cycle payment before joining prebid or auction. | cycle invoice not paid |
| 400 | You have already won this program and cannot join prebid or auction again. | subscriber already won the program |
| 400 | INVALID_AMOUNT | amount is non-positive, outside the configured range, or auction limits are incomplete |
| 400 | A signature is required to submit this prebid. | prebid document required but signatureAssetId missing |
| 400 | A valid signature is required to submit this prebid. | signature asset not found or not READY |
| 400 | A prebid document template is not configured for this program. | required template missing/unpublished |
Coded 400s (
INVALID_AMOUNT,ACTIVE_PREBID_EXISTS,PREBID_DISABLED,PREBID_NOT_OPEN) also carry the stable code aserror.code. Clients must match onerror.code, never on message text.
Edit a Prebid¶
PUT /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId¶
Updates the amount (and optionally the signature/document) of the subscriber's active prebid.
Attributes¶
| Field | Value |
|---|---|
| Method | PUT |
| Path | /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId |
| Auth | Bearer <access> + authorize(['SUBSCRIBER']) |
| Content-Type | application/json |
Path parameters¶
| Param | Type | Required | Description |
|---|---|---|---|
auctionId | cuid2 string | yes | ID of the auction |
prebidId | cuid2 string | yes | ID of the prebid to edit |
Body¶
| Field | Type | Required | Description |
|---|---|---|---|
enrolledSubscriberId | cuid2 string | yes | Enrollment the prebid belongs to; must match the prebid's enrollment |
amount | number | yes | Half-up normalized to two decimals, then validated against bid mode, minimum, and total |
signatureAssetId | cuid2 string | no | New signature asset (creates a fresh document submission) |
Response 200¶
{
"status": "success",
"code": 200,
"message": "Prebid updated successfully.",
"data": {
"auctionId": "cuid2-string",
"cycleId": "cuid2-string",
"prebidId": "cuid2-string",
"status": "ACTIVE",
"documentSubmissionId": "cuid2-string" | null
}
}
Errors¶
Same as Place a Prebid plus:
| Status | Message | When |
|---|---|---|
| 400 | Prebid cannot be updated after submission. | PrebidMutabilityPolicy is IMMUTABLE_CANCEL_ONLY |
| 404 | Active prebid not found. | prebid does not exist, is not ACTIVE, belongs to another auction or subscriber |
Cancel a Prebid¶
DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId¶
Cancels the subscriber's active prebid, freeing the subscriber to place a new one while the prebid window is still open.
Attributes¶
| Field | Value |
|---|---|
| Method | DELETE |
| Path | /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId |
| Auth | Bearer <access> + authorize(['SUBSCRIBER']) |
| Content-Type | application/json |
Path parameters¶
| Param | Type | Required | Description |
|---|---|---|---|
auctionId | cuid2 string | yes | ID of the auction |
prebidId | cuid2 string | yes | ID of the prebid to cancel |
Query parameters¶
| Param | Type | Required | Description |
|---|---|---|---|
enrolledSubscriberId | cuid2 string | yes | Enrollment the prebid belongs to; must match the prebid's enrollment |
No request body.
Response 200¶
{
"status": "success",
"code": 200,
"message": "Prebid cancelled successfully.",
"data": {
"auctionId": "cuid2-string",
"cycleId": "cuid2-string",
"prebidId": "cuid2-string",
"status": "CANCELLED",
"documentSubmissionId": null
}
}
Errors¶
Same as Place a Prebid plus:
| Status | Message | When |
|---|---|---|
| 404 | Active prebid not found. | prebid does not exist, is not ACTIVE, belongs to another auction or subscriber |
Cancellation is not restricted to IMMUTABLE_CANCEL_ONLY — it works for both mutability policies.
Typical Client Flow¶
1. GET /v2/subscribers/me/auctions/:auctionId → read prebidEndAt, prebidPhase, enrolledSubscriberId
2. POST /v2/subscribers/me/auctions/:auctionId/prebid → place → { prebidId, enrolledSubscriberId, status: "ACTIVE" }
3. PUT /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId → edit (only if MUTABLE_UNTIL_CLOSE)
4. DELETE /v2/subscribers/me/auctions/:auctionId/prebid/:prebidId?enrolledSubscriberId=… → cancel
5. POST /v2/subscribers/me/auctions/:auctionId/prebid → re-place after cancel, while window is open
Every call includes the enrolledSubscriberId of the enrollment being acted on (body for POST/PUT, query for DELETE).
All mutations fail with 400 Prebid window is not open. once the prebid end time passes, so the client should treat prebidEndAt from the auction detail response as the hard deadline.
Implementation Details¶
New commands¶
Added under src/application/use-cases/auctions/commands/ and exposed through makeAuctionUseCases as commands.placePrebid, commands.updatePrebid, and commands.cancelSubscriberPrebid:
| Command | File | Key behavior |
|---|---|---|
placePrebid | place-prebid/place-prebid-command.ts | Locks auction by auctionId, resolves participation for the explicit enrolledSubscriberId, enforces the one-active-prebid rule per enrollment, creates a new ACTIVE prebid. |
updatePrebid | update-prebid/update-prebid-command.ts | Locks auction, requires MUTABLE_UNTIL_CLOSE, verifies prebid ownership + enrollment/status via getPrebidById, updates amount/document. |
cancelSubscriberPrebid | cancel-subscriber-prebid/cancel-subscriber-prebid-command.ts | Locks auction, verifies prebid ownership + enrollment/status, sets status to CANCELLED. Policy-agnostic. |
All three take a required enrolledSubscriberId and resolve participation through subscriberProgramsRepository.getParticipationForCycle, which scopes subscriber_programs to that exact enrollment (AND sp.enrolled_subscriber_id = …) and returns 403 You are not part of this cycle. when the subscriber has no such enrollment in the auction's cycle. Prebid records are additionally checked for a matching enrolledSubscriberId on edit/cancel (404 Active prebid not found. on mismatch).
All three run the shared eligibility gate under a row lock (lockAuctionByAuctionId … FOR UPDATE): auction enabled, active cycle, prebid window open, bidder not disqualified, cycle payment completed, not an existing program winner, amount within [minimumBidAmount, totalAmount]. Create and update persist the same canonical two-decimal amount used by live bidding and generated documents. Sealed prebids use no current leading amount, so they are not compared with other subscribers' values or the prior editable value.
Shared prebid document pipeline¶
The ~200-line document-submission flow was extracted from commands/upsert-prebid/upsert-prebid-command.ts into src/application/use-cases/auctions/prebid-submission-helpers.ts:
buildPrebidDocumentSubmission(...)— resolves the prebid template, validates the signature asset, gathers program/company/subscriber/address context, creates thedocument_submissionsrow + audit logs, and returns the submission IDs. Reuses the existing submission when no new signature is given.enqueuePrebidDocumentRendering(...)— queues PDF rendering for a fresh submission and marks itQUEUED; failures are logged, never propagated.
These helpers were extracted from the now-removed legacy cycle-keyed upsertPrebid command, so the whole subscriber prebid surface shares one document pipeline instead of drifting.
Routes¶
src/interfaces/http/routes/v2/subscribers/me/auctions/[auctionId]/prebid/
| File | Purpose |
|---|---|
prebid-routes.ts | Fastify plugin registering POST '', PUT '/:prebidId', DELETE '/:prebidId' with authenticate + authorize(['SUBSCRIBER']) hooks |
prebid-schema.ts | Swagger JSON schemas (params, body, response) |
prebid-validators.ts | Zod validators: CuidSchema for IDs, DecimalSchema for amount |
The plugin is auto-registered in production by @fastify/autoload (the [auctionId] folder maps to the :auctionId route param) and is registered explicitly in tests/helpers/make-test-app.ts for integration tests.
Tests¶
| File | Coverage |
|---|---|
src/application/use-cases/auctions/__tests__/subscriber-prebid-commands.test.ts | 13 unit tests across the three commands: window open/closed, one-active rule, re-place after cancel, immutability, ownership, status guards |
src/interfaces/http/routes/v2/subscribers/me/auctions/[auctionId]/prebid/__tests__/prebid-routes.test.ts | 7 route tests: 200 flows, 401, 403, CUID validation 400, missing amount 400 |
Run with:
Related Documents¶
- auction-and-prebid.md — prebid contract and amount-visibility rules
- auction-list.md — auction listing endpoints
- auction-lifecycle-config.md —
PrebidMutabilityPolicy, presets, and triggers - ../websocket/events/prebid.md — realtime prebid events
- ../../templates/rest-api.md
- ../../development/validation.md
- ../../development/authentication.md
- ../../development/error-handling.md