---
title: "Change a quote status"
method: POST
path: "/quotes/{id}/status"
tags: ["Quotes"]
---

# Change a quote status

`POST /quotes/{id}/status`

## Request

- Base URL: `https://joinways.app/api/v1`
- URL: `https://joinways.app/api/v1/quotes/{id}/status`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://joinways.app/oauth/authorize`, token URL `https://joinways.app/api/oauth/token`, scopes: `mcp.write`)

Same service as the app: timeline entries, amendments, Slack message of an amendment confirmed by hand, `quote.status_changed` webhook.
`accepted` and `signed` are exclusive per event: when another live contract (accepted, signed, invoiced, paid) exists and `force_confirm` is false, nothing is written and the answer is `200` with `success: false`, `conflict: EXISTING_ACCEPTED_QUOTES` and the competing quotes. Call again with `force_confirm: true`: competing `accepted` quotes go back to `pending`; `signed`, `invoiced` and `paid` ones stay and their amounts add up.
A platform quote (`source: platform`) only takes `accepted` or `rejected`; anything else, or a live contract next to it, is a `409` that `force_confirm` does not lift. `invoiced`, `paid` and `cancelled` are set by invoicing and `POST /quotes/{id}/cancel`. **Role permission:** `quotes.manage`.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `status` 'draft' | 'pending' | 'accepted' | 'signed' | 'rejected', required
  - `force_confirm` boolean: Proceed although another live contract exists on the event (see above).

## Response `200`

Status written (`success: true`), or not written because of a competing contract (`success: false`).

- union
  - object
    - `success` true, required
    - `status` 'draft' | 'pending' | 'accepted' | 'signed' | 'rejected' | 'invoiced' | 'paid' | 'cancelled', required: `draft` = not sent yet; `pending` = sent, awaiting the client; `accepted` / `signed` = client agreement (signed = signature recorded); `rejected` = refused; `invoiced` = at least one invoice issued (a deposit included); `paid` = fully settled; `cancelled` = withdrawn. accepted, signed, invoiced and paid are live contracts counted in the event's confirmed amount (unless replaced, see `superseded_by_quote_id`).
  - object
    - `success` false, required
    - `conflict` 'EXISTING_ACCEPTED_QUOTES', required
    - `existing_quotes` object[], required
      - `id` string, uuid
      - `title` string
      - `amount` number, nullable
      - `status` 'draft' | 'pending' | 'accepted' | 'signed' | 'rejected' | 'invoiced' | 'paid' | 'cancelled': `draft` = not sent yet; `pending` = sent, awaiting the client; `accepted` / `signed` = client agreement (signed = signature recorded); `rejected` = refused; `invoiced` = at least one invoice issued (a deposit included); `paid` = fully settled; `cancelled` = withdrawn. accepted, signed, invoiced and paid are live contracts counted in the event's confirmed amount (unless replaced, see `superseded_by_quote_id`).
    - `hint` string

## Other responses

- `400`: Validation failed (`invalid_request`). For a body that fails the schema, `details` holds `formErrors` and `fieldErrors`.
- `401`: Missing, invalid, expired or revoked bearer token (`unauthorized`).
- `403`: `forbidden`: the token lacks the scope, its owner's role lacks the permission named by the operation, or the workspace has neither the Pro plan nor a trial (`details.reason` `plan_not_eligible`).
- `404`: Not found in this workspace, or the id of the path is not a UUID (`not_found`).
- `409`: The quote's state refuses the operation (`conflict`); retrying the same call fails the same way until that state changes (except `amendment_in_flight`, transient). `error.details.reason` (see `QuoteErrorReason`): `quote_locked` (signed, invoiced or paid: amend or cancel instead), `quote_signed`, `quote_cancelled`, `quote_not_cancellable`, `quote_has_active_invoice` (credit the invoice first), `quote_has_amendments` (a quote with a live amendment is frozen), `quote_superseded` (a replaced version is history), `amendment_in_flight` (one amendment at a time), `amendment_nested`, `parent_not_active`, `parent_not_signed`, `parent_fully_invoiced`, `quote_changed` (the status changed meanwhile), `invalid_status`, `not_a_platform_version`, `event_closed` (event lost or cancelled), `event_archived`, `platform_quote_read_only` (a platform quote, `source: platform`, is the platform's document: amounts, discounts, currency and lines are read-only, its status only takes `accepted` or `rejected`), `platform_quote_delete_forbidden` (cancel or refuse it instead), `platform_version_signed` (another version is signed), `quote_conflict` (another live contract must be cancelled or refused first).
- `429`: Too many requests (`rate_limited`). `details.reason`: `per-ip`, `per-account-minute`, `per-account-day`, or `email_rate_limited` (email budget). Always carries `Retry-After`: wait that many seconds.

---

[API](https://skmtc.dev/joinways/apis/joinways-public-api.md) · [All operations](https://skmtc.dev/joinways/apis/joinways-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/joinways/apis/joinways-public-api/revisions/39e84309953e?raw)
