Rebounder Tech Blog

Written by the people who actually run these systems in production.

durationSec 300s Cap Missed at Input, Fails as Permanent 400

Published About 4 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

The booking form never checked total delivery duration against the delivery API's durationSec cap (300s), so orders that passed booking and payment failed only as a permanent 400 at delivery time.

The short version

The booking form’s validation only checked one thing: remaining slot inventory. It verified that units (the number of slots requested) did not exceed remainingUnits. It never checked whether units × slot_seconds — the total delivery seconds the order would occupy — matched the limit enforced by the delivery-side Partner API.

As a result, orders that exceeded the durationSec cap the delivery API enforces via its zod schema (300 seconds) were accepted by the booking form without complaint, and the contract and payment completed normally. The problem surfaced only at the moment the order was actually sent to delivery.

Symptom

Orders with no cap on total delivery seconds passed through booking → contract → payment and only failed once handed to the delivery pipeline.

Before the fix, bookLoopSlotCore went straight from the slot-inventory check to an unrelated check (card-only slot eligibility). No code anywhere computed or validated total seconds.

// src/lib/booking.ts (before)
if ((!loop.canIndividual && !privileged) || units > loop.remainingUnits)
  return {
    ok: false,
    error:
      loop.remainingUnits <= 0
        ? "Sorry, this slot is full. Please choose another."
        : `Only ${loop.remainingUnits} slots remain. Please reduce the number of units.`,
  };
// Card-only slots: fail-closed while Stripe (integrations.is_enabled) is OFF (PR-R2).
// Once enabled, contracts are created with payment_method='card' (0073 integration).
if (loop.card_only && !(await isStripeEnabled()))

The flow goes directly from the inventory check to the card-payment check. No total-duration check exists in between.

Cause

The root cause is that the same value — “the maximum delivery duration one order can use” — was implemented independently, on separate assumptions, in two separate systems: the booking side (kimiteras-portal) and the delivery side (the v2 Partner API).

The delivery side enforces durationSec to the range 1–300 via a zod schema in delivery-payload.ts:

const DURATION_MIN = 1; // ck_ads_duration_positive (> 0)
const DURATION_MAX = 300;
durationSec: z.number().int().min(DURATION_MIN).max(DURATION_MAX),

The module’s own comment states exactly how this validation is meant to behave:

/**
 * ## [Requirement 2] This module's failures = permanent (4xx)
 * Malformed payloads, invalid enums, invalid UUIDs, and empty ad lists are "not
 * fixable by retry," so the route returns **400/422**.
 * Transient DB errors and asset-fetch failures go through a separate path on the
 * route side and return **5xx** (recoverable by retry).
 */

In other words, by design, a payload with durationSec over 300 is treated as a permanent error (400/422) that no retry will fix. That part of the design is correct. The problem is that the booking side — the one accepting the order in the first place — never looked at this 300-second limit at all.

The booking form’s units × loop.slot_seconds is effectively the source data for the durationSec sent to v2. But bookLoopSlotCore neither computed nor validated it; it finalized the order on inventory checks alone. With the default slot configuration (capacity 10 × 30 seconds), the total lands exactly at 300 seconds, so ordinary usage never triggered the bug. Only orders where the unit count was increased past that default — pushing the total over 300 seconds — made it all the way through contract and payment before hitting the delivery-side permanent error.

The fix

A total-duration check was added to bookLoopSlotCore, placed right after the inventory check and before the card-payment check.

// src/lib/booking.ts (after)
// Per-ad cap enforced by v2 delivery (K3 contract: durationSec ≤ 300s). Letting this
// through means an order that completed booking, contract, and payment gets stuck at
// a permanent 400 at delivery time — "sold but will never air" (found in a 2026-07-25 audit).
// The default slot (capacity 10 × 30s) lands at exactly 300s and never trips this,
// so normal bookings are unaffected.
if (units * loop.slot_seconds > 300) {
  const maxUnits = Math.max(1, Math.floor(300 / loop.slot_seconds));
  return {
    ok: false,
    error: `A single booking can cover at most 300 seconds of total airtime (${maxUnits} unit(s) for this slot). Please reduce the number of units.`,
  };
}

Rather than just rejecting the order, it computes 300 ÷ slot_seconds — the maximum number of units allowed for the specific slot being booked — and puts that number directly in the error message. The person booking sees the actual limit for their current slot, not just the raw 300-second ceiling.

What this leaves unresolved

The fix itself just copies a value v2 already enforced — 300 — into a second place on the booking side. Looking at the evidence, this value now exists as two independent literal 300s: one in kimiteras-portal’s booking.ts, and one in the v2 delivery-payload.ts. There is no single source of truth between them.

Because the booking side and the delivery side are separate repositories and separate systems, this duplication was not resolved by this fix. If either one changes in the future without the other, the same category of mismatch can recur.

Frequently asked questions

Q1Why didn't the booking form catch this at input time?

It only checked remaining slot inventory — whether units exceeded remainingUnits. Nothing checked whether units × slot_seconds (the order's total delivery seconds) matched the delivery API's limit.

Q2What does a 'permanent 400' mean here?

The delivery Partner API validates durationSec with a zod schema (min 1, max 300), treating malformed payloads and invalid data as errors no retry can fix. Once an order hits this, every resend returns the same 400 forever, even after booking and payment completed.

Q3How was it fixed?

A check in bookLoopSlotCore now rejects bookings where units × loop.slot_seconds exceeds 300, and computes the max units allowed for that slot (300 ÷ slot_seconds) into the error message.

Environment verified

  • Booking form (kimiteras-portal) → delivery Partner API (K3 contract)
  • Delivery-side durationSec cap: 300 seconds (apps/web/lib/partner/delivery-payload.ts)
  • Found 2026-07-25 in an audit, fixed same day (commit cf19ac2)

What this article is based on

  • TypeScript file lines 1253-1261commit 591b281
  • TypeScript file lines 1262-1271commit cf19ac2
  • TypeScript file lines 20-21commit 255c04e
  • TypeScript file lines 37-38commit 255c04e
  • TypeScript file lines 100-103commit 255c04e

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.