Skip to content

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

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

Authentication

All endpoints require a valid JWT bearer token for a SUBSCRIBER role user.

Authorization: Bearer <token>

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
{
  "enrolledSubscriberId": "cuid2-string",
  "amount": 150,
  "signatureAssetId": "cuid2-string"
}

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 as error.code. Clients must match on error.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)
{
  "enrolledSubscriberId": "cuid2-string",
  "amount": 140
}

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 the document_submissions row + 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 it QUEUED; 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:

pnpm typecheck
pnpm vitest run --config vitest.config.mts src/application/use-cases/auctions