---
title: "List Renewal Decisions"
method: GET
path: "/contracts/renewals"
tags: ["contracts"]
---

# List Renewal Decisions

`GET /contracts/renewals`

Terms still awaiting a renewal decision, soonest deadline first.

Declared above ``GET /{contract_id}``: FastAPI matches in declaration order,
so below it ``renewals`` parses as a contract UUID and 422s.

Read-only, gated by the router's auth only — ``general.file.write`` gates
*recording* a decision, and requiring it here would stop a user who can see
the contract from being told a decision is due on it.

Every filter defaults off, so unqualified this is "every outstanding decision
in the company". My Services asks for both filters: its owner's queue is what
they were written to about and have not finished.

## Query parameters

- `assigned_to_me` boolean — Only terms this user is an assignee of, resolved the same way the reminder digest resolves who to email.
- `within_days` integer, nullable
- `reminded_only` boolean — Only terms whose reminder ladder has reached one of its dates — the decisions the assignees have already been asked about.
- `limit` integer
- `offset` integer

## Response `200`

Successful Response

- ContractRenewalListResponse — Outstanding renewal decisions, soonest deadline first. ``total`` counts the rows the filters matched, not the page — the caller needs it to render a count beside a section header it has only paged part of.
  - `rows` ContractRenewalRowResponse[], required
    - `contract_id` string, uuid, required
    - `contract_title` string, required
    - `vendor_id` string, uuid, nullable
    - `vendor_name` string, nullable
    - `service_names` string[]
    - `term` ContractTermResponse, required — One term period, with the state and countdown derived server-side. ``state`` / ``status`` / ``decision_deadline`` / ``deadline_kind`` / ``days_remaining`` are computed, never stored — the table, the email, and this response all read them from the same helpers so they cannot disagree.
      - `term_id` string, uuid, required
      - `contract_id` string, uuid, required
      - `term_number` integer, required
      - `previous_term_id` string, uuid, nullable
      - `start_date` string, date, nullable
      - `end_date` string, date, nullable
      - `termination_notice_date` string, date, nullable
      - `notice_date_source` 'derived' | 'extracted' | 'manual' — Where a term's ``termination_notice_date`` came from. Drives whether moving ``end_date`` recomputes the notice date: only ``DERIVED`` dates are recomputed, so a human-entered or document-stated date is never silently overwritten.
      - `notice_period_value` integer, nullable
      - `notice_period_unit` 'days' | 'months' — Unit of a termination-notice period. Months are not 30 days: 3 months before 2026-05-31 is 2026-02-28, while 90 days before it is 2026-03-02. This generates a legal deadline, so the unit is stored rather than normalized to days (see ``backend.helpers.contract_terms.derive_notice_date``).
      - `renewal_term_months` integer, nullable
      - `auto_renews` boolean, nullable
      - `decision` 'renew' | 'renegotiate' | 'terminate' | 'replace' — What a user decided to do about a term coming up for renewal. A **fixed** set, not a per-company configurable one: it is what lets the email, the API and the state machine name the same thing without snapshotting a label onto every term. Two behavior pairs (RENEW/RENEGOTIATE, TERMINATE/REPLACE) — same effect, different record.
      - `decision_label` string, nullable
      - `decision_reason` string, nullable
      - `decision_source` 'user' | 'backfill' — Who recorded the decision. No ``SYSTEM_*`` member, deliberately: **nothing but a person records a decision**. A term that runs past its end date undecided stays ``OVERDUE`` and gets surfaced rather than auto-renewed on the customer's behalf — an invented decision is indistinguishable from one they made, and the renewal history is what customers audit.
      - `decided_by_user_id` string, uuid, nullable
      - `decided_by_label` string, nullable
      - `decided_at` string, date-time, nullable
      - `actioned_at` string, date-time, nullable
      - `actioned_by_user_id` string, uuid, nullable
      - `actioned_by_label` string, nullable
      - `state` 'open' | 'overdue' | 'decided' | 'renewed' | 'ended', required — A term's state, **derived** — never stored. Every value is a function of the dates and the decision, so a stored copy would have to be rewritten on each transition and would drift. The predicates live in ``backend.helpers.contract_terms.term_state``.
      - `status` 'overdue' | 'past_notice' | 'pending_decision' | 'pending_action' | 'active' | 'terminated' | 'replaced' | 'renewed' | 'renegotiated', required — Where a term stands, in the one phrase a surface shows for it. A different question from ``ContractTermState``, which is the state machine's own vocabulary. This one tells the **two lapsed dates apart** — a notice window that closed under a term still running is a different problem from a term that ran out entirely — and names outstanding work in the two halves somebody acts on. ``term_status`` derives it; nothing stores it. Declared **most urgent first**, and sorted on that order rather than on the labels, so ascending puts the work at the top of a list.
      - `decision_deadline` string, date, nullable
      - `deadline_kind` 'notice' | 'renewal' — Which of a term's two dates a reminder counts down to. A term has two real deadlines, not one derived deadline: the date you must give notice by, and the date the agreement turns over. Each gets its own ladder off its own cadence, because they are different distances from "act now" — a notice deadline wants months of warning, a renewal date wants days. Who receives a row is **not** encoded here: both configured audiences get every row, resolved at send time (``resolve_term_assignees``).
      - `days_remaining` integer, nullable
      - `in_effect` boolean
      - `attention_due` boolean
      - `notice_reminders_enabled` boolean
      - `renewal_reminders_enabled` boolean
      - `next_notice_reminder_date` string, date, nullable
      - `next_renewal_reminder_date` string, date, nullable
      - `notifications` ContractTermNotificationResponse[]
        - `notification_id` string, uuid, required
        - `lead_days` integer, required
        - `scheduled_for` string, date, required
        - `deadline_kind` 'notice' | 'renewal', required — Which of a term's two dates a reminder counts down to. A term has two real deadlines, not one derived deadline: the date you must give notice by, and the date the agreement turns over. Each gets its own ladder off its own cadence, because they are different distances from "act now" — a notice deadline wants months of warning, a renewal date wants days. Who receives a row is **not** encoded here: both configured audiences get every row, resolved at send time (``resolve_term_assignees``).
        - `target_date` string, date, required
        - `status` 'pending' | 'sent', required — Two values only: cancelling a reminder deletes the unsent row. ``AnnualKeyDates`` has three overlapping "off" states because it kept cancelled rows around. The audit of *why* reminders stopped lives on the term (``decided_at`` / ``decided_by_user_id``), not here.
        - `sent_at` string, date-time, nullable
      - `created` string, date-time, nullable
      - `modified` string, date-time, nullable
  - `total` integer, required

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/kobaltlabs/apis/fastapi.md) · [All operations](https://skmtc.dev/kobaltlabs/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/kobaltlabs/fastapi/revisions/425d5b8a3c17/schema)
