---
title: "Add Contract Document"
method: POST
path: "/contracts/{contract_id}/documents"
tags: ["contracts"]
---

# Add Contract Document

`POST /contracts/{contract_id}/documents`

## Path parameters

- `contract_id` string, uuid, required

## Response `200`

Successful Response

- ContractResponse
  - `contract_id` string, uuid, required
  - `company_id` string, uuid, required
  - `vendor_id` string, uuid, nullable
  - `title` string, required
  - `status` 'draft' | 'active' | 'expired' | 'terminated', required — ``DRAFT`` and ``TERMINATED`` are human judgments; the derivation in ``recompute_contract_status`` overwrites neither.
  - `effective_date` string, date, nullable
  - `expiration_date` string, date, nullable
  - `auto_renews` boolean, nullable
  - `current_term` ContractTermResponse — 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
  - `latest_term` ContractTermResponse — 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
  - `term_history` ContractTermResponse[]
    - `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
  - `obligations` string[]
  - `latest_extraction_run_id` string, uuid, nullable
  - `latest_summary_run_id` string, uuid, nullable
  - `summary_run_status` 'pending' | 'running' | 'completed' | 'failed'
  - `base_file_id` string, uuid, nullable
  - `base_filename` string, nullable
  - `base_upload_id` string, uuid, nullable
  - `documents` ContractDocumentResponse[]
    - `file_id` string, uuid, required
    - `contract_id` string, uuid, required
    - `role` 'base' | 'amendment' | 'addendum' | 'sow' | 'order_form' | 'exhibit', required — How a file relates to its parent Contract. BASE is the root/master document (at most one per contract). All other roles hang off the agreement via ``parent_file_id``.
    - `filename` string, required
    - `parent_file_id` string, uuid, nullable
    - `effective_date` string, date, nullable
    - `upload_id` string, uuid, nullable
    - `expiration_date` string, date, nullable
    - `agent_run_id` string, uuid, nullable
    - `agent_run_status` 'pending' | 'running' | 'completed' | 'failed'
    - `field_groups` FieldGroup[]
      - `id` string, required
      - `label` string, required
      - `fields` Field[], required
        - `id` string, required
        - `label` string, required
        - `value` union, required
          - FieldValueText
            - `kind` 'text', required
            - `value` string, required
          - FieldValueDate
            - `kind` 'date', required
            - `value` string, required
          - FieldValueNumber
            - `kind` 'number', required
            - `value` number, required
          - FieldValueMoney
            - `kind` 'money', required
            - `value` string, required
          - FieldValueYesNo
            - `kind` 'yesno', required
            - `value` boolean, required
          - FieldValueList
            - `kind` 'list', required
            - `value` string[], required
          - FieldValueMultiline
            - `kind` 'multiline', required
            - `value` string, required
        - `citations` BackendModelsPydanticFieldGroupsCitation[]
          - `page` integer, required
          - `content` string, required
    - `versions` ContractDocumentVersionResponse[]
      - `upload_id` string, uuid, required
      - `filename` string, required
      - `uploaded_at` string, date-time, required
      - `expiration_date` string, date, nullable
      - `agent_run_id` string, uuid, nullable
      - `agent_run_status` 'pending' | 'running' | 'completed' | 'failed'
      - `field_groups` FieldGroup[]
        - `id` string, required
        - `label` string, required
        - `fields` Field[], required
          - `id` string, required
          - `label` string, required
          - `value` union, required
            - FieldValueText
              - …
            - FieldValueDate
              - …
            - FieldValueNumber
              - …
            - FieldValueMoney
              - …
            - FieldValueYesNo
              - …
            - FieldValueList
              - …
            - FieldValueMultiline
              - …
          - `citations` BackendModelsPydanticFieldGroupsCitation[]
            - `page` integer, required
            - `content` string, required
  - `services` ContractServiceSummary[]
    - `service_id` string, uuid, required
    - `name` string, required
  - `custom_field_values` union[]
    - union
      - CustomFieldDropdownValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'DROPDOWN'
        - `option_id` string, uuid, nullable
        - `label` string, nullable
        - `hex_color` string, nullable
        - `order_index` integer, nullable
      - CustomFieldFreeTextValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'FREE_TEXT'
        - `text_value` string, nullable
      - CustomFieldNumberValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'NUMBER'
        - `number_value` integer, nullable
      - CustomFieldDateValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'DATE'
        - `date_value` string, date, nullable
      - CustomFieldBooleanValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'BOOLEAN'
        - `boolean_value` boolean, nullable
      - CustomFieldMultiselectValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'MULTISELECT'
        - `selected_options` CustomFieldMultiselectOptionValue[]
          - `option_id` string, uuid, nullable
          - `label` string, nullable
          - `hex_color` string, nullable
          - `order_index` integer, nullable
      - CustomFieldUserValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'USER'
        - `user_id` string, uuid, nullable
        - `user_first_name` string, nullable
        - `user_last_name` string, nullable
        - `email_address` string, nullable
      - CustomFieldMultiUserValue
        - `custom_field_id` string, uuid, nullable
        - `custom_field_type` 'INHERENT_RISK_LEVEL' | 'RESIDUAL_RISK_LEVEL' | 'CRITICALITY' | 'LIFECYCLE_STATUS'
        - `name` string, nullable
        - `type` 'MULTI_USER'
        - `selected_users` CustomFieldUserRef[]
          - `user_id` string, uuid, nullable
          - `user_first_name` string, nullable
          - `user_last_name` string, nullable
          - `email_address` string, nullable
  - `created` string, date-time, nullable
  - `modified` string, date-time, nullable

## 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/3ff4c0bd276b/schema)
