Skip to content

Auction Timing in Seconds — Frontend Integration Guide

The auction API now uses seconds for configured auction durations and automatic extensions. This is a breaking contract change. The previous minute-based keys are no longer accepted or returned.

This change allows configurations such as a 90-second auction and a 30-second late-bid extension without using fractional values.

1. Breaking field changes

Replace these keys everywhere in frontend request types, response types, form state, fixtures, mocks, and cached data:

Removed field Replacement Value conversion
auctionDurationMinutes auctionDurationSeconds old value multiplied by 60
auctionExtensionMinutes auctionExtensionSeconds old value multiplied by 60
cycleAuctionDurationMinutes cycleAuctionDurationSeconds old value multiplied by 60

Examples:

Previous value New value
30 minutes 1800 seconds
2 minutes 120 seconds
90 seconds was unsupported 90 seconds
30-second extension was unsupported 30 seconds

Do not keep aliases or fallback reads for the removed keys. Clear persisted frontend caches when releasing this change because cached minute-valued objects do not match the new contract.

2. Affected API surfaces

The seconds-based names apply to every auction configuration layer:

  • Platform auction policy.
  • Company auction policy and resolved company policy.
  • Auction presets.
  • Program creation and editing.
  • Program auction settings.
  • Per-auction scheduling overrides.
  • Staff and subscriber auction list/detail responses.
  • Cycle, invoice, payment, statement, and program responses that expose auction timing.
  • Auction settings included in REST snapshots.

The main response shapes now use:

interface AuctionTimingSettings {
  auctionDurationSeconds: number | null;
  auctionExtensionSeconds: number;
  auctionFirstCallSeconds: number;
  auctionSecondCallSeconds: number;
  auctionThirdCallSeconds: number;
  subscriberJoinGraceSeconds: number;
  subscriberJoinLeadSeconds: number;
}

interface AuctionTimingSummary {
  auctionStartAt: string | null;
  auctionDurationSeconds: number | null;
  auctionEndAt: string | null;
  closingPhase: 'FIRST_CALL' | 'SECOND_CALL' | 'THIRD_CALL' | null;
  closingPhaseEndsAt: string | null;
}

interface CycleAuctionTimingSummary {
  cycleAuctionDurationSeconds: number | null;
}

auctionExtensionSeconds normally appears inside the resolved settings object. auctionDurationSeconds can also appear at the auction or cycle summary level where the existing endpoint already exposes timing.

3. Validation and form behavior

Use integer seconds in API payloads:

Field Validation Meaning
auctionDurationSeconds integer, 1..86400, or null where the endpoint permits clearing/inheritance Configured duration for DURATION_MODE
auctionExtensionSeconds integer, >= 0, or null on inheritable policy layers Late-bid extension and extension window
subscriberJoinLeadSeconds integer, >= 0, or null on inheritable policy layers How long before auction start joining opens

An extension value of 0 disables automatic extension. A null value on a policy/configuration layer means inherit from the next lower layer. Omission in a patch means no change.

subscriberJoinLeadSeconds defaults to 600. A value of 0 moves the opening boundary to the auction start time. The server-provided joinWindow.opensAt remains the countdown source of truth after scheduling or rescheduling.

Render both duration fields as one numeric input followed by a unit selector:

[ value ] [ Seconds | Minutes ]

Use this control for both auction duration and automatic extension. Keep the selected unit in frontend form state and normalize the value to seconds before building the API payload:

export type AuctionTimeUnit = 'SECONDS' | 'MINUTES';

export interface AuctionTimeInput {
  value: number;
  unit: AuctionTimeUnit;
}

export function toApiSeconds(input: AuctionTimeInput): number {
  if (!Number.isInteger(input.value) || input.value < 0) {
    throw new Error('Time value must be a non-negative integer.');
  }

  return input.unit === 'MINUTES' ? input.value * 60 : input.value;
}

export function fromApiSeconds(totalSeconds: number): AuctionTimeInput {
  if (totalSeconds % 60 === 0) {
    return { value: totalSeconds / 60, unit: 'MINUTES' };
  }

  return { value: totalSeconds, unit: 'SECONDS' };
}

Examples:

User value Selected unit API value
90 Seconds auctionDurationSeconds: 90
2 Minutes auctionDurationSeconds: 120
30 Seconds auctionExtensionSeconds: 30
1 Minutes auctionExtensionSeconds: 60

The backend stores and returns only total seconds. It does not retain whether the user originally selected seconds or minutes. When initializing an edit form, the example above displays values divisible by 60 as minutes and all other values as seconds. If the product must preserve the user's last selected unit, store that preference in frontend state or frontend-owned preferences.

For duration, validate the converted value as 1..86400. For extension, validate it as >= 0; zero disables extension. Keep API values in seconds and convert to milliseconds only at the countdown boundary.

4. Scheduling a 90-second auction

Company route:

PATCH /v2/companies/auctions/:auctionId/schedule
Authorization: Bearer <token>
Content-Type: application/json

Admin route:

PATCH /v2/admin/auctions/:auctionId/schedule
Authorization: Bearer <token>
Content-Type: application/json

Request:

{
  "auctionStartAt": "2026-09-10T10:00:00.000Z",
  "auctionDurationSeconds": 90,
  "auctionClosingMode": "DURATION_MODE",
  "auctionExtensionSeconds": 30,
  "reason": "Configure short auction"
}

The effective initial end is 2026-09-10T10:01:30.000Z. Read the returned auctionEndAt instead of constructing the production timer from the request. The server can adjust the effective end for pauses and accepted late bids.

5. Countdown source of truth

Use the deadline for the active stage. During an active call phase, closingPhaseEndsAt is authoritative. Otherwise, use auctionEndAt. Do not repeatedly derive either deadline from configured seconds on the client.

The server calculates a duration-mode deadline as:

startedAt or auctionStartAt
+ auctionDurationSeconds
+ accumulated paused time
+ accumulated automatic-extension time

The pause and extension accumulators use milliseconds internally. They do not change the unit of the public configuration fields.

A basic countdown can use the pause timestamp as its frozen reference:

export function remainingAuctionMs(params: {
  auctionEndAt: string | null;
  closingPhase: 'FIRST_CALL' | 'SECOND_CALL' | 'THIRD_CALL' | null;
  closingPhaseEndsAt: string | null;
  status: string;
  pausedAt: string | null;
  nowMs?: number;
}): number | null {
  const {
    auctionEndAt,
    closingPhase,
    closingPhaseEndsAt,
    status,
    pausedAt,
    nowMs = Date.now()
  } = params;
  const deadline =
    closingPhase !== null && closingPhaseEndsAt !== null
      ? closingPhaseEndsAt
      : auctionEndAt;
  if (deadline === null) return null;

  const referenceMs =
    status === 'PAUSED' && pausedAt !== null ? Date.parse(pausedAt) : nowMs;
  return Math.max(0, Date.parse(deadline) - referenceMs);
}

After resumed, replace local timing with the new REST snapshot or transition payload before restarting the countdown.

For CALL_MODE, the duration deadline controls when FIRST_CALL starts. Once closingPhase is non-null, render that phase and count down to the returned closingPhaseEndsAt. Bids remain valid until that deadline. Each subsequent call transition replaces both fields. auctionDurationSeconds may be null when another workflow starts the first call.

6. Automatic extension behavior

For DURATION_MODE, an accepted bid inside the final auctionExtensionSeconds window adds the full configured extension to the existing effective end.

With a 30-second extension:

  1. Current auctionEndAt is 10:01:30.
  2. A bid is accepted at 10:01:10, inside the final 30 seconds.
  3. The new auctionEndAt is 10:02:00.
  4. Another qualifying bid can extend it again to 10:02:30.

The configured auctionDurationSeconds stays unchanged. Extensions accumulate as runtime state, so the frontend must not overwrite the configured duration after an extension.

The server publishes an auction.state.changed event:

{
  "type": "auction.state.changed",
  "data": {
    "auctionId": "auction-id",
    "cycleId": "cycle-id",
    "stateVersion": 8,
    "serverSequence": 27,
    "payload": {
      "transition": "extended",
      "state": {
        "status": "LIVE",
        "stateVersion": 8,
        "auctionEndAt": "2026-09-10T10:02:00.000Z"
      }
    }
  }
}

Apply the new auctionEndAt only when the event's stateVersion is newer than the last applied auction state. Duplicate events with the same version should be ignored.

interface AuctionClockState {
  stateVersion: number;
  auctionEndAt: string | null;
}

export function applyAuctionTimingEvent(
  state: AuctionClockState,
  event: {
    stateVersion: number;
    payload: {
      transition: string;
      state: { stateVersion: number; auctionEndAt: string | null };
    };
  }
): AuctionClockState {
  if (event.stateVersion <= state.stateVersion) return state;

  return {
    stateVersion: event.stateVersion,
    auctionEndAt: event.payload.state.auctionEndAt
  };
}

If a version is skipped, the socket reconnects, or the app returns from the background, refetch the auction detail endpoint and replace the local clock state with the REST response.

7. Frontend release checklist

  • Replace all three removed minute-based fields in TypeScript types and API clients.
  • Convert hard-coded defaults, fixtures, and form initial values from minutes to seconds.
  • Render duration and extension as a numeric input with a Seconds/Minutes selector.
  • Convert the selected value to integer seconds before validation and submission.
  • Send integer seconds in policy, preset, program, and schedule writes.
  • Read auctionEndAt as the live deadline.
  • During FIRST_CALL, SECOND_CALL, or THIRD_CALL, use closingPhaseEndsAt as the active bid deadline.
  • Handle auction.state.changed with transition: "call_started"; replace closingPhase and closingPhaseEndsAt from its payload.
  • Handle auction.state.changed with transition: "extended" and update the deadline using stateVersion ordering.
  • regularAuctionEndAt is the duration deadline before extensions; auctionEndAt is the effective deadline including extensions.
  • extensionStartedAt remains null until the regular deadline is crossed. A qualifying bid emits extended immediately when it moves the effective deadline. The lifecycle worker later emits exactly one auction.state.changed event with transition: "extension_started" at the regular boundary.
  • On reconnect, replace all three timing fields from REST/snapshot state. Do not infer that extension time has begun only because the effective deadline moved.
  • Refetch auction state after reconnect, a sequence/version gap, resume, or app foregrounding.
  • Keep auctionDurationSeconds unchanged when a late bid extends the auction.
  • Clear persisted API/query caches during deployment.
  • Remove tests and fixtures that still expect minute-based names.

Related guides: