---
title: "Apply Promo"
method: POST
path: "/carts/{cart_id}/promo"
tags: ["Carts"]
---

# Apply Promo

`POST /carts/{cart_id}/promo`

## Request

- Base URL: `http://localhost:8000`
- URL: `http://localhost:8000/carts/{cart_id}/promo`
- Auth: none declared

Apply a promo code to a cart (Cart-of-1).

Persists ``cart.promo_code`` + ``cart.campaign_id`` and per-line
``discount_amount`` + ``pms_discount_id``. Idempotent under
``Idempotency-Key`` (5-minute replay window). Failures surface as
typed ``CartServiceError`` / ``CampaignServiceError`` (the standard
handlers in ``app.main`` build the envelope).

## Path parameters

- `cart_id` integer, required

## Headers

- `authorization` string, nullable
- `Idempotency-Key` string, nullable
- `API-Version` '1'

## Cookies

- `tp_cart` string, nullable

## Request body

- ApplyPromoRequest: ``POST /carts/{id}/promo`` body. The ``code`` is canonicalised server-side (uppercase + trim + allowlist regex). Whitespace-only / empty / overly long / illegal-character inputs are rejected with ``error_code_invalid``.
  - `code` string, required: Promo code to apply. Case-insensitive; canonicalised server-side.

## Response `200`

Successful Response

- CartResponseEnvelope: Standard response wrapper returned by every cart endpoint.
  - `cart` CartSchema, required: Full cart state returned from every mutation.
    - `id` integer, required
    - `status` string, required
    - `expires_at` string, date-time, required
    - `billing_guest` object, nullable
    - `promo_code` string, nullable
    - `campaign_id` integer, nullable
    - `commit_strategy` string
    - `currency` string
    - `created_via` string, nullable
    - `embed_integrator` string, nullable
    - `embed_parent_origin` string, nullable
    - `stays` CartStaySchema[]
      - `id` integer, required
      - `position` integer, required
      - `property_id` integer, required
      - `pms_provider` string, required
      - `lead_guest_override` object, nullable
      - `special_requests` string, nullable
      - `booking_intent_id` integer, nullable
      - `pricing_source` string
      - `quoted_at` string, date-time, nullable
      - `last_checked_at` string, date-time, nullable: When this stay's cached price and availability were last checked. Null until the first check. The cart's last-checked time is the oldest of these.
      - `status` string, required
      - `error_code` string, nullable
      - `error_message` string, nullable
      - `retried_from_stay_id` integer, nullable: On a cart built by ``POST /carts/{id}/retry``: the unwound stay this stay was copied from.
      - `group_booking` GroupBookingEligibility: Whether this stay may hold more than one line (Phase 2 group booking). ``eligible`` is true only when the global switch, the property opt-in, and PMS support all hold. Clients show "Add another room/site" only when it is true and the stay has fewer than ``max_lines`` lines.
        - `eligible` boolean
        - `max_lines` integer
      - `multi_property` MultiPropertyCartEligibility: Whether this stay may share a cart with stays at other properties. ``eligible`` is true only when the global switch, the park opt-out, PMS support, ``payment_path == 'platform'``, and the live-booking channel all hold (``is_multi_property_cart_eligible``), and the cart is not an embed cart. Clients show "Add a stay at another park" only when every stay is eligible and the cart has fewer than ``max_stays`` stays; ``add_stay`` refuses a second stay when any stay fails its own gate.
        - `eligible` boolean
        - `max_stays` integer
      - `lines` CartLineSchema[]
        - `id` integer, required
        - `position` integer, required
        - `room_type_id` integer, required
        - `rate_plan_id` integer, nullable
        - `area_id` string, nullable
        - `check_in` string, date, required
        - `check_out` string, date, required
        - `adults` integer, required
        - `children` integer
        - `infants` integer
        - `pet_declaration` object, nullable
        - `equipment` object, nullable
        - `lead_guest_override` object, nullable
        - `total_amount` string, nullable: Accommodation subtotal as string decimal.
        - `pet_fee_total` string, nullable: Pet fee total as string decimal.
        - `discount_amount` string, nullable: Discount applied as string decimal.
        - `rates_snapshot` object, nullable
        - `booking_id` integer, nullable
        - `pms_reservation_id` string, nullable
        - `pms_confirmation_number` string, nullable
        - `special_requests` string, nullable
    - `totals` CartTotals: Rolled-up totals for the entire cart.
      - `accommodation` string: Sum of line accommodation subtotals.
      - `pet_fee` string: Sum of line pet fees.
      - `discount` string: Sum of line discounts.
      - `service_fee` string: Sum of per-stay TP platform fees. Each stay is 1% of its post-discount subtotal, with a $1 floor and $5 cap.
      - `grand_total` string: Guest-pays total.
      - `currency` string
    - `payment_intent` PaymentIntentInfo: Subset of Stripe PaymentIntent surfaced to the client.
      - `id` string, required
      - `client_secret` string, required
      - `amount` integer, required
      - `currency` string, required
      - `statement_descriptor_suffix` string, nullable: The suffix this PaymentIntent carries: the park name (first 10 characters) for one park, ``TOTALPARKS`` for a multi-stay cart. Null on a Connect charge.
      - `statement_descriptor` string, nullable: The card statement line, ``{prefix}* {suffix}``. Null when the platform prefix is not configured. Show it as is; never rebuild it.
    - `checkout_code` string, nullable: The ``TPC-`` display code of a cart with 2+ stays, issued at its first prepare (D-04). The guest quotes it to Total Parks. It is not a lookup key.
    - `retried_from` RetriedFromInfo: The unwound cart a retry cart was built from (Phase 4 Stage 6a).
      - `cart_id` integer, required
      - `checkout_code` string, nullable: The unwound checkout's ``TPC-`` display code.
      - `payment_status` string, nullable: What happened to the first payment: ``refunded`` (after capture) or ``cancelled`` (released, never charged).
      - `grand_total` string, required: The first payment's amount.
      - `currency` string
  - `applied_changes` AppliedChange[]
    - `type` string, required: Change kind: cart_created, stay_added, line_added, line_updated, stay_removed, line_removed, user_linked, cart_updated, prepared, confirmed.
    - `stay_id` integer, nullable
    - `line_id` integer, nullable
    - `user_id` integer, nullable
    - `extra` object, nullable
  - `warnings` CartWarning[]
    - `code` string, required
    - `stay_id` integer, nullable
    - `line_id` integer, nullable
    - `reason` string, required
    - `details` object, nullable
  - `errors` CartErrorEnvelope[]
    - `error_code` string, required
    - `error_category` 'validation' | 'auth' | 'rate_limit' | 'pms_unavailable' | 'payment' | 'internal', required
    - `message` string, required
    - `recovery_hint` string, nullable
    - `retryable` boolean
    - `details` object, nullable
  - `cart_token` string, nullable: Plaintext cart token — returned ONCE on create and link-user.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/totalparks/apis/total-parks-public-api.md) · [All operations](https://skmtc.dev/totalparks/apis/total-parks-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/totalparks/apis/total-parks-public-api/revisions/dcb4c822e2c6?raw)
