Commit Diff


commit - /dev/null
commit + 68d918221826c5615baf78e1b2acb3579bae7121
blob - /dev/null
blob + 567609b1234a9b8806c5a05da6c866e480aa148d (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1 @@
+build/
blob - /dev/null
blob + 32f3292e31bbd5acf75b7671b679e9017828078c (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + 34a0d2e46c69be74f746f0e0a22808f60e3d2f52 (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,114 @@
+#
+# Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+RUSTC ?= $(shell rustup which rustc 2>/dev/null || which rustc)
+RUSTFLAGS ?= -C opt-level=2
+VERSION = 0.1.0
+PREFIX ?= /usr/local
+
+BUILD = build
+SRC = shortcut_sdk.rs
+LIB = $(BUILD)/libshortcut_sdk.rlib
+TEST = $(BUILD)/shortcut_sdk-test
+
+EXAMPLE_SRCS = $(wildcard examples/*.rs)
+EXAMPLES = $(EXAMPLE_SRCS:examples/%.rs=$(BUILD)/ex-%)
+
+# Vendored deps managed by marmita(1).  Adding vendor/foo.rs is enough
+# for the pattern rule below to build it as an rlib; if the upstream
+# ships link.mk, marmita copies it to vendor/foo.mk and the -include
+# below appends to LINK_FLAGS / BUILD_ENV.
+VENDOR_RS    = $(wildcard vendor/*.rs)
+VENDOR_NAMES = $(VENDOR_RS:vendor/%.rs=%)
+VENDOR_LIBS  = $(VENDOR_NAMES:%=$(BUILD)/lib%.rlib)
+VENDOR_EXT   = $(foreach n,$(VENDOR_NAMES),--extern $(n)=$(BUILD)/lib$(n).rlib)
+LINK_FLAGS  =
+BUILD_ENV   =
+-include $(wildcard vendor/*.mk)
+LINK_ARGS   = $(addprefix -C link-arg=,$(LINK_FLAGS))
+
+CLIPPY ?= $(shell rustup which clippy-driver 2>/dev/null)
+RUSTFMT ?= $(shell rustup which rustfmt 2>/dev/null)
+RUSTDOC ?= $(shell rustup which rustdoc 2>/dev/null || which rustdoc)
+
+DOC = $(BUILD)/doc/shortcut_sdk/index.html
+
+.PHONY: all clean test fmt-check clippy ci doc examples install
+
+all: $(LIB)
+
+$(BUILD)/lib%.rlib: vendor/%.rs
+	mkdir -p $(BUILD)
+	$(BUILD_ENV) $(RUSTC) --edition 2024 --crate-type rlib \
+		--crate-name $* $(RUSTFLAGS) -o $@ $<
+
+$(LIB): $(SRC) $(VENDOR_LIBS)
+	mkdir -p $(BUILD)
+	SHORTCUT_SDK_VERSION=$(VERSION) $(RUSTC) --edition 2024 \
+		--crate-type rlib --crate-name shortcut_sdk $(RUSTFLAGS) \
+		$(VENDOR_EXT) \
+		$(LINK_ARGS) \
+		-o $@ $<
+
+$(TEST): $(SRC) $(VENDOR_LIBS)
+	mkdir -p $(BUILD)
+	SHORTCUT_SDK_VERSION=$(VERSION) $(RUSTC) --edition 2024 \
+		--test --crate-name shortcut_sdk \
+		$(VENDOR_EXT) \
+		$(LINK_ARGS) \
+		-o $@ $<
+
+test: $(TEST)
+	$(TEST)
+
+fmt-check:
+	$(RUSTFMT) --edition 2024 --check $(SRC)
+
+clippy: $(VENDOR_LIBS)
+	mkdir -p $(BUILD)
+	SHORTCUT_SDK_VERSION=$(VERSION) $(CLIPPY) --edition 2024 \
+		--crate-type rlib --crate-name shortcut_sdk \
+		$(VENDOR_EXT) \
+		$(LINK_ARGS) \
+		-W clippy::all -o $(BUILD)/shortcut_sdk.clippy $(SRC)
+	@rm -f $(BUILD)/shortcut_sdk.clippy
+
+$(DOC): $(SRC) $(VENDOR_LIBS)
+	mkdir -p $(BUILD)
+	SHORTCUT_SDK_VERSION=$(VERSION) $(RUSTDOC) --edition 2024 \
+		--crate-name shortcut_sdk \
+		$(VENDOR_EXT) \
+		$(LINK_ARGS) \
+		-o $(BUILD)/doc $(SRC)
+
+doc: $(DOC)
+
+$(BUILD)/ex-%: examples/%.rs $(LIB)
+	mkdir -p $(BUILD)
+	SHORTCUT_SDK_VERSION=$(VERSION) $(RUSTC) --edition 2024 \
+		--crate-name $* --extern shortcut_sdk=$(LIB) -L $(BUILD) \
+		$(RUSTFLAGS) -o $@ $<
+
+examples: $(EXAMPLES)
+
+install: $(LIB)
+	install -d $(PREFIX)/lib
+	install -m 644 $(LIB) $(PREFIX)/lib/libshortcut_sdk.rlib
+
+clean:
+	rm -rf $(BUILD)
+
+ci: fmt-check clippy $(LIB) test
blob - /dev/null
blob + bd001e8158c65a43c8054dc15bf35df83bc56789 (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,111 @@
+shortcut_sdk - Shortcut REST API v3 client
+==========================================
+shortcut_sdk is a minimal Rust library distributed as a single source file
+(`shortcut_sdk.rs`).  It wraps the Shortcut REST API v3 with typed
+request and response structs and a thin `Client` over HTTP.
+
+
+Requirements
+------------
+rustc with the 2024 edition, plus libtls and libcrypto for the vendored
+http crate.  No cargo, no procedural macros, no async runtime.
+
+
+Building
+--------
+Build the rlib:
+
+    make
+
+Run the unit tests:
+
+    make test
+
+Generate browsable rustdoc under `build/doc/shortcut_sdk/index.html`:
+
+    make doc
+
+Format and lint everything (used in CI):
+
+    make ci
+
+
+Vendoring into your project
+---------------------------
+shortcut_sdk depends on two vendored single-file crates: `http` and
+`jackson` (both shipped under `vendor/`).  Copy `shortcut_sdk.rs` and
+the contents of `vendor/` into your project, then build them as rlibs
+and link your binary against all three:
+
+    rustc --edition 2024 --crate-type rlib --crate-name http \
+        -o build/libhttp.rlib vendor/http.rs
+    rustc --edition 2024 --crate-type rlib --crate-name jackson \
+        -o build/libjackson.rlib vendor/jackson.rs
+    SHORTCUT_SDK_VERSION=0.1.0 \
+    rustc --edition 2024 --crate-type rlib --crate-name shortcut_sdk \
+        --extern http=build/libhttp.rlib \
+        --extern jackson=build/libjackson.rlib \
+        -C link-arg=-ltls -C link-arg=-lcrypto \
+        -o build/libshortcut_sdk.rlib shortcut_sdk.rs
+
+Your binary then links against shortcut_sdk:
+
+    rustc --edition 2024 --crate-type bin \
+        --extern shortcut_sdk=build/libshortcut_sdk.rlib \
+        --extern http=build/libhttp.rlib \
+        --extern jackson=build/libjackson.rlib \
+        -L build -C link-arg=-ltls -C link-arg=-lcrypto \
+        -o build/myapp main.rs
+
+The Makefile in this repository follows the same pattern; reading it is
+the shortest path to wiring up your own.
+
+
+Quick tour
+----------
+Authenticate with a Shortcut API token (set the `SHORTCUT_TOKEN`
+environment variable in production code):
+
+    use shortcut_sdk::{Client, CreateStoryParams, SearchQuery};
+
+    let c = Client::new(std::env::var("SHORTCUT_TOKEN")?);
+
+Read a story, list stories from a group, search across the workspace:
+
+    let s = c.get_story(1234)?;
+    let inbox = c.list_group_stories("uuid-platform", Some(50), None)?;
+    let hits = c.search_stories(
+        &SearchQuery::new("owner:ijanc state:in-progress").page_size(25),
+    )?;
+
+Create a story with the fluent params builder (optional fields stay
+unset and are not sent on the wire):
+
+    let s = c.create_story(
+        &CreateStoryParams::default()
+            .name("Write SDK docs")
+            .workflow_state_id(500_000_123)
+            .owner_ids(vec!["uuid-1".into()]),
+    )?;
+
+Upload a PRD as an attachment to a story:
+
+    use shortcut_sdk::FileUpload;
+
+    c.upload_files(
+        &[FileUpload::new("PRD.pdf", "application/pdf")],
+        Some(s.id),
+    )?;
+
+
+Download
+--------
+    got clone ssh://ijanc@ijanc.org/shortcut_sdk
+    git clone https://git.ijanc.org/shortcut_sdk.git
+    git clone https://git.sr.ht/~ijanc/shortcut_sdk
+    git clone https://github.com/shortcut_sdk.git
+
+
+License
+-------
+ISC -- see LICENSE.
blob - /dev/null
blob + 450307cba41d678fd5dc8c231a18f64e1d9d26b3 (mode 644)
--- /dev/null
+++ shortcut_sdk.rs
@@ -0,0 +1,3745 @@
+// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et :
+//
+// Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+//! Shortcut REST API v3 client.
+
+use std::fmt;
+
+use jackson::{FromJson, ToJson, Value, json_struct};
+
+pub fn version() -> &'static str {
+    env!("SHORTCUT_SDK_VERSION")
+}
+
+const BASE_URL: &str = "https://api.app.shortcut.com";
+const TOKEN_HEADER: &str = "Shortcut-Token";
+
+//////////////////////////////////////////////////////////////////////////////
+// Error
+//////////////////////////////////////////////////////////////////////////////
+
+pub enum Error {
+    Http(String),
+    Status { code: u16, body: String },
+    Decode(String),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Error::Http(m) => write!(f, "http: {m}"),
+            Error::Status { code, body } => {
+                write!(f, "status {code}: {}", body.trim())
+            }
+            Error::Decode(m) => write!(f, "decode: {m}"),
+        }
+    }
+}
+
+impl fmt::Debug for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
+
+impl std::error::Error for Error {}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+//////////////////////////////////////////////////////////////////////////////
+// Client
+//////////////////////////////////////////////////////////////////////////////
+
+pub struct Client {
+    token: String,
+    base: String,
+}
+
+impl Client {
+    pub fn new(token: impl Into<String>) -> Self {
+        Self {
+            token: token.into(),
+            base: BASE_URL.to_string(),
+        }
+    }
+
+    pub fn with_base(token: impl Into<String>, base: impl Into<String>) -> Self {
+        Self {
+            token: token.into(),
+            base: base.into(),
+        }
+    }
+
+    fn request(&self, method: http::Method, path: &str) -> http::RequestBuilder {
+        http::request(method, format!("{}{}", self.base, path))
+            .header(TOKEN_HEADER, self.token.clone())
+            .header("Accept", "application/json")
+            .header("User-Agent", format!("shortcut_sdk/{}", version()))
+    }
+
+    fn json_call<T: FromJson, B: ToJson>(
+        &self,
+        method: http::Method,
+        path: &str,
+        body: &B,
+    ) -> Result<T> {
+        let json = body
+            .to_json()
+            .stringify()
+            .map_err(|e| Error::Decode(e.to_string()))?;
+        let rb = self
+            .request(method, path)
+            .header("Content-Type", "application/json")
+            .body(json);
+        send_json(rb)
+    }
+}
+
+fn send_json<T: FromJson>(rb: http::RequestBuilder) -> Result<T> {
+    let resp = rb.send().map_err(|e| Error::Http(e.to_string()))?;
+    let code = resp.status.code();
+    let body = resp.body_string().map_err(|e| Error::Http(e.to_string()))?;
+    if !(200..300).contains(&code) {
+        return Err(Error::Status { code, body });
+    }
+    let v: Value = body
+        .parse()
+        .map_err(|e: jackson::Error| Error::Decode(e.to_string()))?;
+    T::from_json(&v).map_err(|e| Error::Decode(e.to_string()))
+}
+
+fn send_empty(rb: http::RequestBuilder) -> Result<()> {
+    let resp = rb.send().map_err(|e| Error::Http(e.to_string()))?;
+    let code = resp.status.code();
+    if !(200..300).contains(&code) {
+        let body = resp.body_string().unwrap_or_default();
+        return Err(Error::Status { code, body });
+    }
+    Ok(())
+}
+
+// Append non-None query params to `path`, percent-encoding values.
+fn with_query(path: &str, params: &[(&str, Option<String>)]) -> String {
+    let mut out = String::from(path);
+    let mut sep = '?';
+    for (k, v) in params {
+        if let Some(val) = v {
+            out.push(sep);
+            sep = '&';
+            out.push_str(k);
+            out.push('=');
+            out.push_str(&http::percent_encode(val));
+        }
+    }
+    out
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// params_struct! - struct + Default + ToJson(skip None) + fluent setters
+//
+// Each field is tagged `required` (typed as T, always emitted) or `opt`
+// (typed as Option<T>, emitted only when Some).  Setters consume self and
+// take `impl Into<T>` so the call site does not handle the Option.
+//////////////////////////////////////////////////////////////////////////////
+
+macro_rules! params_struct {
+    (
+        $(#[$meta:meta])*
+        $vis:vis struct $name:ident {
+            $( $kind:ident $field:ident : $ty:ty ),* $(,)?
+        }
+    ) => {
+        $(#[$meta])*
+        $vis struct $name {
+            $( pub $field: params_struct!(@ty $kind $ty), )*
+        }
+
+        impl ::std::default::Default for $name {
+            fn default() -> Self {
+                Self {
+                    $( $field: params_struct!(@default $kind $ty), )*
+                }
+            }
+        }
+
+        impl $crate::ToJson for $name {
+            #[allow(clippy::vec_init_then_push)]
+            fn to_json(&self) -> $crate::Value {
+                let mut __out: ::std::vec::Vec<
+                    (::std::string::String, $crate::Value)
+                > = ::std::vec::Vec::new();
+                $( params_struct!(@emit self, __out, $kind $field); )*
+                $crate::Value::Object(__out)
+            }
+        }
+
+        impl $name {
+            $( params_struct!(@setter $kind $field $ty); )*
+        }
+    };
+
+    (@ty required $ty:ty) => { $ty };
+    (@ty opt $ty:ty) => { ::std::option::Option<$ty> };
+
+    (@default required $ty:ty) => {
+        <$ty as ::std::default::Default>::default()
+    };
+    (@default opt $ty:ty) => { ::std::option::Option::None };
+
+    (@emit $self:ident, $out:ident, required $field:ident) => {
+        $out.push((
+            ::std::stringify!($field).to_string(),
+            $crate::ToJson::to_json(&$self.$field),
+        ));
+    };
+    (@emit $self:ident, $out:ident, opt $field:ident) => {
+        if let ::std::option::Option::Some(__v) = &$self.$field {
+            $out.push((
+                ::std::stringify!($field).to_string(),
+                $crate::ToJson::to_json(__v),
+            ));
+        }
+    };
+
+    (@setter required $field:ident $ty:ty) => {
+        pub fn $field(
+            mut self,
+            value: impl ::std::convert::Into<$ty>,
+        ) -> Self {
+            self.$field = value.into();
+            self
+        }
+    };
+    (@setter opt $field:ident $ty:ty) => {
+        pub fn $field(
+            mut self,
+            value: impl ::std::convert::Into<$ty>,
+        ) -> Self {
+            self.$field = ::std::option::Option::Some(value.into());
+            self
+        }
+    };
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Icon - shared between Member.profile.display_icon and Group.display_icon
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Icon {
+        pub id: String,
+        pub entity_type: String,
+        pub url: String,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Epic
+//
+// Feature-level grouping of stories.  Epic.health is the only place this
+// commit consumes Health; the health endpoints land in their own commit.
+//
+// EpicState carries a wire field "type" (Rust keyword); same workaround
+// as WorkflowState -- hand-written FromJson/ToJson, Rust field is `kind`.
+//
+// ThreadedComment is recursive (a comment can have child comments).
+// Vec<ThreadedComment> on the field is enough to break the cycle since
+// Vec is heap-allocated.
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Health {
+        pub id: Option<String>,
+        pub entity_type: String,
+        pub status: String,
+        pub text: Option<String>,
+        pub author_id: Option<String>,
+        pub epic_id: Option<i64>,
+        pub objective_id: Option<i64>,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct EpicAssociatedGroup {
+        pub group_id: String,
+        pub associated_stories_count: Option<i64>,
+    }
+}
+
+json_struct! {
+    pub struct EpicStats {
+        pub num_points: i64,
+        pub num_points_done: i64,
+        pub num_points_started: i64,
+        pub num_points_unstarted: i64,
+        pub num_points_backlog: i64,
+        pub num_stories_total: i64,
+        pub num_stories_done: i64,
+        pub num_stories_started: i64,
+        pub num_stories_unstarted: i64,
+        pub num_stories_backlog: i64,
+        pub num_stories_unestimated: i64,
+        pub num_related_documents: i64,
+        pub last_story_update: Option<String>,
+    }
+}
+
+pub struct EpicState {
+    pub id: i64,
+    pub global_id: String,
+    pub entity_type: String,
+    pub name: String,
+    pub description: String,
+    pub color: Option<String>,
+    pub kind: String,
+    pub position: i64,
+    pub created_at: String,
+    pub updated_at: String,
+}
+
+impl ToJson for EpicState {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("id".into(), self.id.to_json()),
+            ("global_id".into(), self.global_id.to_json()),
+            ("entity_type".into(), self.entity_type.to_json()),
+            ("name".into(), self.name.to_json()),
+            ("description".into(), self.description.to_json()),
+            ("color".into(), self.color.to_json()),
+            ("type".into(), self.kind.to_json()),
+            ("position".into(), self.position.to_json()),
+            ("created_at".into(), self.created_at.to_json()),
+            ("updated_at".into(), self.updated_at.to_json()),
+        ])
+    }
+}
+
+impl FromJson for EpicState {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut id = None;
+        let mut global_id = None;
+        let mut entity_type = None;
+        let mut name = None;
+        let mut description = None;
+        let mut color = None;
+        let mut kind = None;
+        let mut position = None;
+        let mut created_at = None;
+        let mut updated_at = None;
+        for (k, val) in obj {
+            match k.as_str() {
+                "id" => id = Some(val),
+                "global_id" => global_id = Some(val),
+                "entity_type" => entity_type = Some(val),
+                "name" => name = Some(val),
+                "description" => description = Some(val),
+                "color" => color = Some(val),
+                "type" => kind = Some(val),
+                "position" => position = Some(val),
+                "created_at" => created_at = Some(val),
+                "updated_at" => updated_at = Some(val),
+                _ => {}
+            }
+        }
+        macro_rules! req {
+            ($opt:ident, $key:expr) => {
+                $opt.ok_or_else(|| jackson::Error::new(concat!("missing field: ", $key), 0))
+                    .and_then(FromJson::from_json)?
+            };
+        }
+        Ok(EpicState {
+            id: req!(id, "id"),
+            global_id: req!(global_id, "global_id"),
+            entity_type: req!(entity_type, "entity_type"),
+            name: req!(name, "name"),
+            description: req!(description, "description"),
+            color: match color {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            kind: req!(kind, "type"),
+            position: req!(position, "position"),
+            created_at: req!(created_at, "created_at"),
+            updated_at: req!(updated_at, "updated_at"),
+        })
+    }
+}
+
+json_struct! {
+    pub struct EpicWorkflow {
+        pub id: i64,
+        pub entity_type: String,
+        pub default_epic_state_id: i64,
+        pub epic_states: Vec<EpicState>,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+json_struct! {
+    pub struct ThreadedComment {
+        pub id: i64,
+        pub app_url: String,
+        pub entity_type: String,
+        pub text: String,
+        pub author_id: String,
+        pub deleted: bool,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub external_id: Option<String>,
+        pub comments: Vec<ThreadedComment>,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+json_struct! {
+    pub struct Epic {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: String,
+        pub state: String,
+        pub epic_state_id: i64,
+        pub milestone_id: Option<i64>,
+        pub group_id: Option<String>,
+        pub group_ids: Vec<String>,
+        pub project_ids: Vec<i64>,
+        pub objective_ids: Vec<i64>,
+        pub label_ids: Vec<i64>,
+        pub labels: Vec<LabelSlim>,
+        pub follower_ids: Vec<String>,
+        pub owner_ids: Vec<String>,
+        pub requested_by_id: String,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub associated_groups: Vec<EpicAssociatedGroup>,
+        pub external_id: Option<String>,
+        pub position: i64,
+        pub stories_without_projects: i64,
+        pub started: bool,
+        pub started_at: Option<String>,
+        pub started_at_override: Option<String>,
+        pub completed: bool,
+        pub completed_at: Option<String>,
+        pub completed_at_override: Option<String>,
+        pub planned_start_date: Option<String>,
+        pub deadline: Option<String>,
+        pub archived: bool,
+        pub stats: EpicStats,
+        pub health: Option<Health>,
+        pub comments: Vec<ThreadedComment>,
+        pub productboard_id: Option<String>,
+        pub productboard_plugin_id: Option<String>,
+        pub productboard_url: Option<String>,
+        pub productboard_name: Option<String>,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct EpicSlim {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: Option<String>,
+        pub state: String,
+        pub epic_state_id: i64,
+        pub milestone_id: Option<i64>,
+        pub group_id: Option<String>,
+        pub group_ids: Vec<String>,
+        pub project_ids: Vec<i64>,
+        pub objective_ids: Vec<i64>,
+        pub label_ids: Vec<i64>,
+        pub labels: Vec<LabelSlim>,
+        pub follower_ids: Vec<String>,
+        pub owner_ids: Vec<String>,
+        pub requested_by_id: String,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub associated_groups: Vec<EpicAssociatedGroup>,
+        pub external_id: Option<String>,
+        pub position: i64,
+        pub stories_without_projects: i64,
+        pub started: bool,
+        pub started_at: Option<String>,
+        pub started_at_override: Option<String>,
+        pub completed: bool,
+        pub completed_at: Option<String>,
+        pub completed_at_override: Option<String>,
+        pub planned_start_date: Option<String>,
+        pub deadline: Option<String>,
+        pub archived: bool,
+        pub stats: EpicStats,
+        pub productboard_id: Option<String>,
+        pub productboard_plugin_id: Option<String>,
+        pub productboard_url: Option<String>,
+        pub productboard_name: Option<String>,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct EpicPaginatedResults {
+        pub total: i64,
+        pub data: Vec<EpicSlim>,
+        pub next: Option<i64>,
+    }
+}
+
+params_struct! {
+    pub struct CreateEpicParams {
+        required name: String,
+        opt description: String,
+        opt state: String,
+        opt epic_state_id: i64,
+        opt milestone_id: i64,
+        opt group_id: String,
+        opt group_ids: Vec<String>,
+        opt objective_ids: Vec<i64>,
+        opt labels: Vec<CreateLabelParams>,
+        opt follower_ids: Vec<String>,
+        opt owner_ids: Vec<String>,
+        opt requested_by_id: String,
+        opt external_id: String,
+        opt converted_from_story_id: i64,
+        opt planned_start_date: String,
+        opt deadline: String,
+        opt started_at_override: String,
+        opt completed_at_override: String,
+        opt created_at: String,
+        opt updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateEpicParams {
+        opt name: String,
+        opt description: String,
+        opt state: String,
+        opt epic_state_id: i64,
+        opt milestone_id: i64,
+        opt group_id: String,
+        opt group_ids: Vec<String>,
+        opt objective_ids: Vec<i64>,
+        opt labels: Vec<CreateLabelParams>,
+        opt follower_ids: Vec<String>,
+        opt owner_ids: Vec<String>,
+        opt requested_by_id: String,
+        opt external_id: String,
+        opt archived: bool,
+        opt before_id: i64,
+        opt after_id: i64,
+        opt planned_start_date: String,
+        opt deadline: String,
+        opt started_at_override: String,
+        opt completed_at_override: String,
+    }
+}
+
+impl Client {
+    pub fn list_epics(&self, includes_description: Option<bool>) -> Result<Vec<EpicSlim>> {
+        let path = with_query(
+            "/api/v3/epics",
+            &[(
+                "includes_description",
+                includes_description.map(|b| b.to_string()),
+            )],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn list_epics_paginated(
+        &self,
+        includes_description: Option<bool>,
+        page: Option<i64>,
+        page_size: Option<i64>,
+    ) -> Result<EpicPaginatedResults> {
+        let path = with_query(
+            "/api/v3/epics/paginated",
+            &[
+                (
+                    "includes_description",
+                    includes_description.map(|b| b.to_string()),
+                ),
+                ("page", page.map(|n| n.to_string())),
+                ("page_size", page_size.map(|n| n.to_string())),
+            ],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn get_epic(&self, id: i64) -> Result<Epic> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/epics/{id}")))
+    }
+
+    pub fn create_epic(&self, params: &CreateEpicParams) -> Result<Epic> {
+        self.json_call(http::Method::Post, "/api/v3/epics", params)
+    }
+
+    pub fn update_epic(&self, id: i64, params: &UpdateEpicParams) -> Result<Epic> {
+        self.json_call(http::Method::Put, &format!("/api/v3/epics/{id}"), params)
+    }
+
+    pub fn delete_epic(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/epics/{id}")))
+    }
+
+    pub fn list_epic_stories(
+        &self,
+        id: i64,
+        includes_description: Option<bool>,
+    ) -> Result<Vec<StorySlim>> {
+        let path = with_query(
+            &format!("/api/v3/epics/{id}/stories"),
+            &[(
+                "includes_description",
+                includes_description.map(|b| b.to_string()),
+            )],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn get_epic_workflow(&self) -> Result<EpicWorkflow> {
+        send_json(self.request(http::Method::Get, "/api/v3/epic-workflow"))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Category
+//
+// Tag for grouping objectives (and the legacy "milestones" view).
+// Category.type is a Rust keyword -- hand-written FromJson and ToJson,
+// Rust field is `kind`.  Subresources /categories/{id}/objectives and
+// /categories/{id}/milestones return the legacy Milestone payload and
+// stay deferred (Milestone is out of v1).
+//////////////////////////////////////////////////////////////////////////////
+
+pub struct Category {
+    pub id: i64,
+    pub global_id: String,
+    pub entity_type: String,
+    pub name: String,
+    pub kind: String,
+    pub color: Option<String>,
+    pub external_id: Option<String>,
+    pub archived: bool,
+    pub created_at: String,
+    pub updated_at: String,
+}
+
+impl ToJson for Category {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("id".into(), self.id.to_json()),
+            ("global_id".into(), self.global_id.to_json()),
+            ("entity_type".into(), self.entity_type.to_json()),
+            ("name".into(), self.name.to_json()),
+            ("type".into(), self.kind.to_json()),
+            ("color".into(), self.color.to_json()),
+            ("external_id".into(), self.external_id.to_json()),
+            ("archived".into(), self.archived.to_json()),
+            ("created_at".into(), self.created_at.to_json()),
+            ("updated_at".into(), self.updated_at.to_json()),
+        ])
+    }
+}
+
+impl FromJson for Category {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut id = None;
+        let mut global_id = None;
+        let mut entity_type = None;
+        let mut name = None;
+        let mut kind = None;
+        let mut color = None;
+        let mut external_id = None;
+        let mut archived = None;
+        let mut created_at = None;
+        let mut updated_at = None;
+        for (k, val) in obj {
+            match k.as_str() {
+                "id" => id = Some(val),
+                "global_id" => global_id = Some(val),
+                "entity_type" => entity_type = Some(val),
+                "name" => name = Some(val),
+                "type" => kind = Some(val),
+                "color" => color = Some(val),
+                "external_id" => external_id = Some(val),
+                "archived" => archived = Some(val),
+                "created_at" => created_at = Some(val),
+                "updated_at" => updated_at = Some(val),
+                _ => {}
+            }
+        }
+        macro_rules! req {
+            ($opt:ident, $key:expr) => {
+                $opt.ok_or_else(|| jackson::Error::new(concat!("missing field: ", $key), 0))
+                    .and_then(FromJson::from_json)?
+            };
+        }
+        Ok(Category {
+            id: req!(id, "id"),
+            global_id: req!(global_id, "global_id"),
+            entity_type: req!(entity_type, "entity_type"),
+            name: req!(name, "name"),
+            kind: req!(kind, "type"),
+            color: match color {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            external_id: match external_id {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            archived: req!(archived, "archived"),
+            created_at: req!(created_at, "created_at"),
+            updated_at: req!(updated_at, "updated_at"),
+        })
+    }
+}
+
+params_struct! {
+    pub struct CreateCategoryParams {
+        required name: String,
+        opt color: String,
+        opt external_id: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateCategoryParams {
+        opt name: String,
+        opt color: String,
+        opt archived: bool,
+    }
+}
+
+impl Client {
+    pub fn list_categories(&self) -> Result<Vec<Category>> {
+        send_json(self.request(http::Method::Get, "/api/v3/categories"))
+    }
+
+    pub fn get_category(&self, id: i64) -> Result<Category> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/categories/{id}")))
+    }
+
+    pub fn create_category(&self, params: &CreateCategoryParams) -> Result<Category> {
+        self.json_call(http::Method::Post, "/api/v3/categories", params)
+    }
+
+    pub fn update_category(&self, id: i64, params: &UpdateCategoryParams) -> Result<Category> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/categories/{id}"),
+            params,
+        )
+    }
+
+    pub fn delete_category(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/categories/{id}")))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Key Result
+//
+// Key-results sit under objectives.  The wire `type` field clashes with
+// Rust; same `kind` rename pattern.  KeyResultValue is a tagged union of
+// numeric vs boolean readings; both fields are optional and only one is
+// populated at a time.
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct KeyResultValue {
+        pub numeric_value: Option<String>,
+        pub boolean_value: Option<bool>,
+    }
+}
+
+pub struct KeyResult {
+    pub id: String,
+    pub name: String,
+    pub objective_id: i64,
+    pub kind: String,
+    pub progress: i64,
+    pub initial_observed_value: KeyResultValue,
+    pub current_observed_value: KeyResultValue,
+    pub current_target_value: KeyResultValue,
+}
+
+impl ToJson for KeyResult {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("id".into(), self.id.to_json()),
+            ("name".into(), self.name.to_json()),
+            ("objective_id".into(), self.objective_id.to_json()),
+            ("type".into(), self.kind.to_json()),
+            ("progress".into(), self.progress.to_json()),
+            (
+                "initial_observed_value".into(),
+                self.initial_observed_value.to_json(),
+            ),
+            (
+                "current_observed_value".into(),
+                self.current_observed_value.to_json(),
+            ),
+            (
+                "current_target_value".into(),
+                self.current_target_value.to_json(),
+            ),
+        ])
+    }
+}
+
+impl FromJson for KeyResult {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut id = None;
+        let mut name = None;
+        let mut objective_id = None;
+        let mut kind = None;
+        let mut progress = None;
+        let mut initial = None;
+        let mut current_observed = None;
+        let mut current_target = None;
+        for (k, val) in obj {
+            match k.as_str() {
+                "id" => id = Some(val),
+                "name" => name = Some(val),
+                "objective_id" => objective_id = Some(val),
+                "type" => kind = Some(val),
+                "progress" => progress = Some(val),
+                "initial_observed_value" => initial = Some(val),
+                "current_observed_value" => current_observed = Some(val),
+                "current_target_value" => current_target = Some(val),
+                _ => {}
+            }
+        }
+        macro_rules! req {
+            ($opt:ident, $key:expr) => {
+                $opt.ok_or_else(|| jackson::Error::new(concat!("missing field: ", $key), 0))
+                    .and_then(FromJson::from_json)?
+            };
+        }
+        Ok(KeyResult {
+            id: req!(id, "id"),
+            name: req!(name, "name"),
+            objective_id: req!(objective_id, "objective_id"),
+            kind: req!(kind, "type"),
+            progress: req!(progress, "progress"),
+            initial_observed_value: req!(initial, "initial_observed_value"),
+            current_observed_value: req!(current_observed, "current_observed_value"),
+            current_target_value: req!(current_target, "current_target_value"),
+        })
+    }
+}
+
+params_struct! {
+    pub struct UpdateKeyResultParams {
+        opt name: String,
+        opt initial_observed_value: KeyResultValue,
+        opt observed_value: KeyResultValue,
+        opt target_value: KeyResultValue,
+    }
+}
+
+impl Client {
+    pub fn get_key_result(&self, id: &str) -> Result<KeyResult> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/key-results/{id}")))
+    }
+
+    pub fn update_key_result(&self, id: &str, params: &UpdateKeyResultParams) -> Result<KeyResult> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/key-results/{id}"),
+            params,
+        )
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Objective
+//
+// Strategic OKR-level container.  Embeds Vec<Category> and exposes its
+// own ObjectiveStats (lead and cycle time aggregates).
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct ObjectiveStats {
+        pub num_related_documents: i64,
+        pub average_cycle_time: Option<i64>,
+        pub average_lead_time: Option<i64>,
+    }
+}
+
+json_struct! {
+    pub struct Objective {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: String,
+        pub state: String,
+        pub categories: Vec<Category>,
+        pub key_result_ids: Vec<String>,
+        pub position: i64,
+        pub started: bool,
+        pub started_at: Option<String>,
+        pub started_at_override: Option<String>,
+        pub completed: bool,
+        pub completed_at: Option<String>,
+        pub completed_at_override: Option<String>,
+        pub archived: bool,
+        pub stats: ObjectiveStats,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct CreateObjectiveParams {
+        required name: String,
+        opt description: String,
+        opt state: String,
+        opt categories: Vec<CreateCategoryParams>,
+        opt started_at_override: String,
+        opt completed_at_override: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateObjectiveParams {
+        opt name: String,
+        opt description: String,
+        opt state: String,
+        opt archived: bool,
+        opt categories: Vec<CreateCategoryParams>,
+        opt before_id: i64,
+        opt after_id: i64,
+        opt started_at_override: String,
+        opt completed_at_override: String,
+    }
+}
+
+impl Client {
+    pub fn list_objectives(&self) -> Result<Vec<Objective>> {
+        send_json(self.request(http::Method::Get, "/api/v3/objectives"))
+    }
+
+    pub fn get_objective(&self, id: i64) -> Result<Objective> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/objectives/{id}")))
+    }
+
+    pub fn create_objective(&self, params: &CreateObjectiveParams) -> Result<Objective> {
+        self.json_call(http::Method::Post, "/api/v3/objectives", params)
+    }
+
+    pub fn update_objective(&self, id: i64, params: &UpdateObjectiveParams) -> Result<Objective> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/objectives/{id}"),
+            params,
+        )
+    }
+
+    pub fn delete_objective(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/objectives/{id}")))
+    }
+
+    pub fn list_objective_epics(&self, id: i64) -> Result<Vec<EpicSlim>> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/objectives/{id}/epics")))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Health endpoints
+//
+// Health updates rate the status of an epic or objective ("on track",
+// "at risk", "off track").  The Health response struct itself lives
+// near Epic since Epic embeds it; this section is just the endpoints
+// and the create/update parameter shapes (which are identical between
+// epics and objectives, so a single CreateHealthParams covers both).
+//////////////////////////////////////////////////////////////////////////////
+
+params_struct! {
+    pub struct CreateHealthParams {
+        required status: String,
+        opt text: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateHealthParams {
+        opt status: String,
+        opt text: String,
+    }
+}
+
+impl Client {
+    pub fn get_epic_health(&self, epic_id: i64) -> Result<Health> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/epics/{epic_id}/health"),
+        ))
+    }
+
+    pub fn create_epic_health(&self, epic_id: i64, params: &CreateHealthParams) -> Result<Health> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/epics/{epic_id}/health"),
+            params,
+        )
+    }
+
+    pub fn list_epic_health_history(&self, epic_id: i64) -> Result<Vec<Health>> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/epics/{epic_id}/health-history"),
+        ))
+    }
+
+    pub fn get_objective_health(&self, objective_id: i64) -> Result<Health> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/objectives/{objective_id}/health"),
+        ))
+    }
+
+    pub fn create_objective_health(
+        &self,
+        objective_id: i64,
+        params: &CreateHealthParams,
+    ) -> Result<Health> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/objectives/{objective_id}/health"),
+            params,
+        )
+    }
+
+    pub fn list_objective_health_history(&self, objective_id: i64) -> Result<Vec<Health>> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/objectives/{objective_id}/health-history"),
+        ))
+    }
+
+    pub fn update_health(&self, id: &str, params: &UpdateHealthParams) -> Result<Health> {
+        self.json_call(http::Method::Put, &format!("/api/v3/health/{id}"), params)
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Document
+//
+// Shortcut's first-class wiki page (PRDs, ADRs, postmortems, design
+// notes).  Doc carries the full content_markdown / content_html;
+// DocSlim is the listing payload (id + title + app_url).
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Doc {
+        pub id: String,
+        pub title: Option<String>,
+        pub content_markdown: Option<String>,
+        pub content_html: Option<String>,
+        pub app_url: String,
+        pub archived: bool,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+json_struct! {
+    pub struct DocSlim {
+        pub id: String,
+        pub title: Option<String>,
+        pub app_url: String,
+    }
+}
+
+json_struct! {
+    pub struct DocSearchResults {
+        pub total: i64,
+        pub data: Vec<DocSlim>,
+        pub next: Option<String>,
+    }
+}
+
+params_struct! {
+    pub struct CreateDocParams {
+        required title: String,
+        required content: String,
+        opt content_format: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateDocParams {
+        opt title: String,
+        opt content: String,
+        opt content_format: String,
+    }
+}
+
+impl Client {
+    pub fn list_docs(&self) -> Result<Vec<DocSlim>> {
+        send_json(self.request(http::Method::Get, "/api/v3/documents"))
+    }
+
+    pub fn get_doc(&self, id: &str) -> Result<Doc> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/documents/{id}")))
+    }
+
+    pub fn create_doc(&self, params: &CreateDocParams) -> Result<DocSlim> {
+        self.json_call(http::Method::Post, "/api/v3/documents", params)
+    }
+
+    pub fn update_doc(&self, id: &str, params: &UpdateDocParams) -> Result<Doc> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/documents/{id}"),
+            params,
+        )
+    }
+
+    pub fn delete_doc(&self, id: &str) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/documents/{id}")))
+    }
+
+    pub fn list_doc_epics(&self, id: &str) -> Result<Vec<EpicSlim>> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/documents/{id}/epics")))
+    }
+
+    pub fn link_doc_to_epic(&self, doc_id: &str, epic_id: i64) -> Result<()> {
+        send_empty(self.request(
+            http::Method::Put,
+            &format!("/api/v3/documents/{doc_id}/epics/{epic_id}"),
+        ))
+    }
+
+    pub fn unlink_doc_from_epic(&self, doc_id: &str, epic_id: i64) -> Result<()> {
+        send_empty(self.request(
+            http::Method::Delete,
+            &format!("/api/v3/documents/{doc_id}/epics/{epic_id}"),
+        ))
+    }
+
+    pub fn list_epic_documents(&self, epic_id: i64) -> Result<Vec<DocSlim>> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/epics/{epic_id}/documents"),
+        ))
+    }
+
+    pub fn search_documents(&self, q: &SearchQuery) -> Result<DocSearchResults> {
+        let path = q.append_to("/api/v3/search/documents");
+        send_json(self.request(http::Method::Get, &path))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Linked-file endpoints
+//
+// LinkedFile struct lives near Story (Story.linked_files embeds it).
+// Both create and update params carry a "type" wire field (Rust keyword);
+// hand-written ToJson for both keeps the call site builder-style while
+// the wire stays "type".
+//////////////////////////////////////////////////////////////////////////////
+
+pub struct CreateLinkedFileParams {
+    pub name: String,
+    pub url: String,
+    pub kind: String,
+    pub story_id: Option<i64>,
+    pub description: Option<String>,
+    pub size: Option<i64>,
+    pub uploader_id: Option<String>,
+    pub content_type: Option<String>,
+    pub thumbnail_url: Option<String>,
+}
+
+impl CreateLinkedFileParams {
+    pub fn new(name: impl Into<String>, url: impl Into<String>, kind: impl Into<String>) -> Self {
+        Self {
+            name: name.into(),
+            url: url.into(),
+            kind: kind.into(),
+            story_id: None,
+            description: None,
+            size: None,
+            uploader_id: None,
+            content_type: None,
+            thumbnail_url: None,
+        }
+    }
+
+    pub fn story_id(mut self, v: i64) -> Self {
+        self.story_id = Some(v);
+        self
+    }
+    pub fn description(mut self, v: impl Into<String>) -> Self {
+        self.description = Some(v.into());
+        self
+    }
+    pub fn size(mut self, v: i64) -> Self {
+        self.size = Some(v);
+        self
+    }
+    pub fn uploader_id(mut self, v: impl Into<String>) -> Self {
+        self.uploader_id = Some(v.into());
+        self
+    }
+    pub fn content_type(mut self, v: impl Into<String>) -> Self {
+        self.content_type = Some(v.into());
+        self
+    }
+    pub fn thumbnail_url(mut self, v: impl Into<String>) -> Self {
+        self.thumbnail_url = Some(v.into());
+        self
+    }
+}
+
+impl ToJson for CreateLinkedFileParams {
+    fn to_json(&self) -> Value {
+        let mut out: Vec<(String, Value)> = Vec::new();
+        out.push(("name".into(), self.name.to_json()));
+        out.push(("url".into(), self.url.to_json()));
+        out.push(("type".into(), self.kind.to_json()));
+        if let Some(v) = self.story_id {
+            out.push(("story_id".into(), v.to_json()));
+        }
+        if let Some(v) = &self.description {
+            out.push(("description".into(), v.to_json()));
+        }
+        if let Some(v) = self.size {
+            out.push(("size".into(), v.to_json()));
+        }
+        if let Some(v) = &self.uploader_id {
+            out.push(("uploader_id".into(), v.to_json()));
+        }
+        if let Some(v) = &self.content_type {
+            out.push(("content_type".into(), v.to_json()));
+        }
+        if let Some(v) = &self.thumbnail_url {
+            out.push(("thumbnail_url".into(), v.to_json()));
+        }
+        Value::Object(out)
+    }
+}
+
+#[derive(Default)]
+pub struct UpdateLinkedFileParams {
+    pub name: Option<String>,
+    pub url: Option<String>,
+    pub kind: Option<String>,
+    pub story_id: Option<i64>,
+    pub description: Option<String>,
+    pub size: Option<i64>,
+    pub uploader_id: Option<String>,
+    pub thumbnail_url: Option<String>,
+}
+
+impl UpdateLinkedFileParams {
+    pub fn name(mut self, v: impl Into<String>) -> Self {
+        self.name = Some(v.into());
+        self
+    }
+    pub fn url(mut self, v: impl Into<String>) -> Self {
+        self.url = Some(v.into());
+        self
+    }
+    pub fn kind(mut self, v: impl Into<String>) -> Self {
+        self.kind = Some(v.into());
+        self
+    }
+    pub fn story_id(mut self, v: i64) -> Self {
+        self.story_id = Some(v);
+        self
+    }
+    pub fn description(mut self, v: impl Into<String>) -> Self {
+        self.description = Some(v.into());
+        self
+    }
+    pub fn size(mut self, v: i64) -> Self {
+        self.size = Some(v);
+        self
+    }
+    pub fn uploader_id(mut self, v: impl Into<String>) -> Self {
+        self.uploader_id = Some(v.into());
+        self
+    }
+    pub fn thumbnail_url(mut self, v: impl Into<String>) -> Self {
+        self.thumbnail_url = Some(v.into());
+        self
+    }
+}
+
+impl ToJson for UpdateLinkedFileParams {
+    fn to_json(&self) -> Value {
+        let mut out: Vec<(String, Value)> = Vec::new();
+        if let Some(v) = &self.name {
+            out.push(("name".into(), v.to_json()));
+        }
+        if let Some(v) = &self.url {
+            out.push(("url".into(), v.to_json()));
+        }
+        if let Some(v) = &self.kind {
+            out.push(("type".into(), v.to_json()));
+        }
+        if let Some(v) = self.story_id {
+            out.push(("story_id".into(), v.to_json()));
+        }
+        if let Some(v) = &self.description {
+            out.push(("description".into(), v.to_json()));
+        }
+        if let Some(v) = self.size {
+            out.push(("size".into(), v.to_json()));
+        }
+        if let Some(v) = &self.uploader_id {
+            out.push(("uploader_id".into(), v.to_json()));
+        }
+        if let Some(v) = &self.thumbnail_url {
+            out.push(("thumbnail_url".into(), v.to_json()));
+        }
+        Value::Object(out)
+    }
+}
+
+impl Client {
+    pub fn list_linked_files(&self) -> Result<Vec<LinkedFile>> {
+        send_json(self.request(http::Method::Get, "/api/v3/linked-files"))
+    }
+
+    pub fn get_linked_file(&self, id: i64) -> Result<LinkedFile> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/linked-files/{id}")))
+    }
+
+    pub fn create_linked_file(&self, params: &CreateLinkedFileParams) -> Result<LinkedFile> {
+        self.json_call(http::Method::Post, "/api/v3/linked-files", params)
+    }
+
+    pub fn update_linked_file(
+        &self,
+        id: i64,
+        params: &UpdateLinkedFileParams,
+    ) -> Result<LinkedFile> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/linked-files/{id}"),
+            params,
+        )
+    }
+
+    pub fn delete_linked_file(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/linked-files/{id}")))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Label endpoints
+//
+// Label and LabelStats live near Iteration (iterations carry Vec<Label>).
+// CreateLabelParams was already defined alongside the story params.
+// This commit adds UpdateLabelParams plus the standalone label CRUD and
+// the label->epic / label->story listings used to filter cross-cutting.
+//////////////////////////////////////////////////////////////////////////////
+
+params_struct! {
+    pub struct UpdateLabelParams {
+        opt name: String,
+        opt description: String,
+        opt color: String,
+        opt archived: bool,
+    }
+}
+
+impl Client {
+    pub fn list_labels(&self, slim: Option<bool>) -> Result<Vec<Label>> {
+        let path = with_query("/api/v3/labels", &[("slim", slim.map(|b| b.to_string()))]);
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn get_label(&self, id: i64) -> Result<Label> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/labels/{id}")))
+    }
+
+    pub fn create_label(&self, params: &CreateLabelParams) -> Result<Label> {
+        self.json_call(http::Method::Post, "/api/v3/labels", params)
+    }
+
+    pub fn update_label(&self, id: i64, params: &UpdateLabelParams) -> Result<Label> {
+        self.json_call(http::Method::Put, &format!("/api/v3/labels/{id}"), params)
+    }
+
+    pub fn delete_label(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/labels/{id}")))
+    }
+
+    pub fn list_label_epics(&self, id: i64) -> Result<Vec<EpicSlim>> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/labels/{id}/epics")))
+    }
+
+    pub fn list_label_stories(
+        &self,
+        id: i64,
+        includes_description: Option<bool>,
+    ) -> Result<Vec<StorySlim>> {
+        let path = with_query(
+            &format!("/api/v3/labels/{id}/stories"),
+            &[(
+                "includes_description",
+                includes_description.map(|b| b.to_string()),
+            )],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// File endpoints
+//
+// UploadedFile already lives near Story (Story.files embeds it).  The
+// upload endpoint is the only multipart/form-data caller in the SDK; it
+// uses the http::Multipart helper from the vendor.  Shortcut accepts up
+// to four files per request via fields named file0..file3 with an
+// optional story_id form field.
+//////////////////////////////////////////////////////////////////////////////
+
+pub struct FileUpload {
+    pub path: ::std::path::PathBuf,
+    pub content_type: String,
+}
+
+impl FileUpload {
+    pub fn new(path: impl Into<::std::path::PathBuf>, content_type: impl Into<String>) -> Self {
+        Self {
+            path: path.into(),
+            content_type: content_type.into(),
+        }
+    }
+}
+
+params_struct! {
+    pub struct UpdateFileParams {
+        opt name: String,
+        opt description: String,
+        opt uploader_id: String,
+        opt external_id: String,
+        opt created_at: String,
+        opt updated_at: String,
+    }
+}
+
+impl Client {
+    pub fn list_files(&self) -> Result<Vec<UploadedFile>> {
+        send_json(self.request(http::Method::Get, "/api/v3/files"))
+    }
+
+    pub fn get_file(&self, id: i64) -> Result<UploadedFile> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/files/{id}")))
+    }
+
+    pub fn update_file(&self, id: i64, params: &UpdateFileParams) -> Result<UploadedFile> {
+        self.json_call(http::Method::Put, &format!("/api/v3/files/{id}"), params)
+    }
+
+    pub fn delete_file(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/files/{id}")))
+    }
+
+    /// Upload up to four files in a single request.  `story_id`
+    /// optionally attaches the uploaded files to a story.
+    pub fn upload_files(
+        &self,
+        files: &[FileUpload],
+        story_id: Option<i64>,
+    ) -> Result<Vec<UploadedFile>> {
+        if files.is_empty() {
+            return Err(Error::Decode(
+                "upload_files: at least one file is required".into(),
+            ));
+        }
+        if files.len() > 4 {
+            return Err(Error::Decode(
+                "upload_files: at most 4 files per request".into(),
+            ));
+        }
+        let mut form = http::Multipart::new();
+        if let Some(sid) = story_id {
+            form = form.text("story_id", sid.to_string());
+        }
+        for (i, f) in files.iter().enumerate() {
+            form = form
+                .file_path(format!("file{i}"), &f.path, f.content_type.clone())
+                .map_err(|e| Error::Http(e.to_string()))?;
+        }
+        let rb = self
+            .request(http::Method::Post, "/api/v3/files")
+            .multipart(form);
+        send_json(rb)
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Group
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Group {
+        pub id: String,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub mention_name: String,
+        pub description: String,
+        pub archived: bool,
+        pub color: Option<String>,
+        pub color_key: Option<String>,
+        pub display_icon: Option<Icon>,
+        pub default_workflow_id: Option<i64>,
+        pub member_ids: Vec<String>,
+        pub workflow_ids: Vec<i64>,
+        pub num_stories: i64,
+        pub num_stories_started: i64,
+        pub num_stories_backlog: i64,
+        pub num_epics_started: i64,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct CreateGroupParams {
+        required name: String,
+        required mention_name: String,
+        opt description: String,
+        opt member_ids: Vec<String>,
+        opt workflow_ids: Vec<i64>,
+        opt color: String,
+        opt color_key: String,
+        opt display_icon_id: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateGroupParams {
+        opt name: String,
+        opt mention_name: String,
+        opt description: String,
+        opt archived: bool,
+        opt color: String,
+        opt color_key: String,
+        opt display_icon_id: String,
+        opt default_workflow_id: i64,
+        opt member_ids: Vec<String>,
+        opt workflow_ids: Vec<i64>,
+    }
+}
+
+impl Client {
+    pub fn list_groups(&self, archived: Option<bool>) -> Result<Vec<Group>> {
+        let path = with_query(
+            "/api/v3/groups",
+            &[("archived", archived.map(|b| b.to_string()))],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn get_group(&self, id: &str) -> Result<Group> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/groups/{id}")))
+    }
+
+    pub fn create_group(&self, params: &CreateGroupParams) -> Result<Group> {
+        self.json_call(http::Method::Post, "/api/v3/groups", params)
+    }
+
+    pub fn update_group(&self, id: &str, params: &UpdateGroupParams) -> Result<Group> {
+        self.json_call(http::Method::Put, &format!("/api/v3/groups/{id}"), params)
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Member
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Profile {
+        pub id: String,
+        pub entity_type: String,
+        pub name: Option<String>,
+        pub mention_name: String,
+        pub email_address: Option<String>,
+        pub gravatar_hash: Option<String>,
+        pub display_icon: Option<Icon>,
+        pub deactivated: bool,
+        pub two_factor_auth_activated: Option<bool>,
+        pub is_owner: bool,
+        pub is_agent: Option<bool>,
+    }
+}
+
+json_struct! {
+    pub struct Member {
+        pub id: String,
+        pub global_id: String,
+        pub entity_type: String,
+        pub role: String,
+        pub state: String,
+        pub disabled: bool,
+        pub created_without_invite: bool,
+        pub group_ids: Vec<String>,
+        pub installation_id: Option<String>,
+        pub replaced_by: Option<String>,
+        pub profile: Profile,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct BasicWorkspaceInfo {
+        pub id: String,
+        pub name: String,
+        pub url_slug: String,
+        pub utc_offset: String,
+        pub default_workflow_id: i64,
+        pub estimate_scale: Vec<i32>,
+        pub korey_enabled: Option<bool>,
+        pub created_at: String,
+    }
+}
+
+json_struct! {
+    pub struct MemberInfoOrganization2 {
+        pub id: String,
+    }
+}
+
+json_struct! {
+    pub struct MemberInfo {
+        pub id: String,
+        pub name: String,
+        pub mention_name: String,
+        pub role: String,
+        pub is_owner: bool,
+        pub workspace2: BasicWorkspaceInfo,
+        pub organization2: MemberInfoOrganization2,
+    }
+}
+
+impl Client {
+    pub fn list_members(
+        &self,
+        org_public_id: Option<&str>,
+        disabled: Option<bool>,
+    ) -> Result<Vec<Member>> {
+        let path = with_query(
+            "/api/v3/members",
+            &[
+                ("org-public-id", org_public_id.map(str::to_string)),
+                ("disabled", disabled.map(|b| b.to_string())),
+            ],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn get_member(&self, id: &str, org_public_id: Option<&str>) -> Result<Member> {
+        let path = with_query(
+            &format!("/api/v3/members/{id}"),
+            &[("org-public-id", org_public_id.map(str::to_string))],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn get_current_member_info(&self) -> Result<MemberInfo> {
+        send_json(self.request(http::Method::Get, "/api/v3/member"))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Iteration
+//
+// Sprints / time-boxed buckets of stories.  IterationStats carries the
+// per-iteration throughput numbers (points done, lead/cycle time means)
+// that drive velocity reports.
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct IterationAssociatedGroup {
+        pub group_id: String,
+        pub associated_stories_count: Option<i64>,
+    }
+}
+
+json_struct! {
+    pub struct IterationStats {
+        pub num_points: i64,
+        pub num_points_done: i64,
+        pub num_points_started: i64,
+        pub num_points_unstarted: i64,
+        pub num_points_backlog: i64,
+        pub num_stories_done: i64,
+        pub num_stories_started: i64,
+        pub num_stories_unstarted: i64,
+        pub num_stories_backlog: i64,
+        pub num_stories_unestimated: i64,
+        pub num_related_documents: i64,
+        pub average_cycle_time: Option<i64>,
+        pub average_lead_time: Option<i64>,
+    }
+}
+
+json_struct! {
+    pub struct LabelStats {
+        pub num_epics: i64,
+        pub num_epics_completed: i64,
+        pub num_epics_in_progress: i64,
+        pub num_epics_total: i64,
+        pub num_epics_unstarted: i64,
+        pub num_points_backlog: i64,
+        pub num_points_completed: i64,
+        pub num_points_in_progress: i64,
+        pub num_points_total: i64,
+        pub num_points_unstarted: i64,
+        pub num_related_documents: i64,
+        pub num_stories_backlog: i64,
+        pub num_stories_completed: i64,
+        pub num_stories_in_progress: i64,
+        pub num_stories_total: i64,
+        pub num_stories_unestimated: i64,
+        pub num_stories_unstarted: i64,
+    }
+}
+
+json_struct! {
+    pub struct Label {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: Option<String>,
+        pub color: Option<String>,
+        pub archived: bool,
+        pub external_id: Option<String>,
+        pub stats: Option<LabelStats>,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct Iteration {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: String,
+        pub status: String,
+        pub start_date: String,
+        pub end_date: String,
+        pub group_ids: Vec<String>,
+        pub follower_ids: Vec<String>,
+        pub label_ids: Vec<i64>,
+        pub labels: Vec<Label>,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub associated_groups: Vec<IterationAssociatedGroup>,
+        pub stats: IterationStats,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+json_struct! {
+    pub struct IterationSlim {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub status: String,
+        pub start_date: String,
+        pub end_date: String,
+        pub group_ids: Vec<String>,
+        pub follower_ids: Vec<String>,
+        pub label_ids: Vec<i64>,
+        pub labels: Vec<Label>,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub associated_groups: Vec<IterationAssociatedGroup>,
+        pub stats: IterationStats,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct CreateIterationParams {
+        required name: String,
+        required start_date: String,
+        required end_date: String,
+        opt description: String,
+        opt group_ids: Vec<String>,
+        opt follower_ids: Vec<String>,
+        opt labels: Vec<CreateLabelParams>,
+    }
+}
+
+params_struct! {
+    pub struct UpdateIterationParams {
+        opt name: String,
+        opt start_date: String,
+        opt end_date: String,
+        opt description: String,
+        opt group_ids: Vec<String>,
+        opt follower_ids: Vec<String>,
+        opt labels: Vec<CreateLabelParams>,
+    }
+}
+
+impl Client {
+    pub fn list_iterations(&self) -> Result<Vec<IterationSlim>> {
+        send_json(self.request(http::Method::Get, "/api/v3/iterations"))
+    }
+
+    pub fn get_iteration(&self, id: i64) -> Result<Iteration> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/iterations/{id}")))
+    }
+
+    pub fn create_iteration(&self, params: &CreateIterationParams) -> Result<Iteration> {
+        self.json_call(http::Method::Post, "/api/v3/iterations", params)
+    }
+
+    pub fn update_iteration(&self, id: i64, params: &UpdateIterationParams) -> Result<Iteration> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/iterations/{id}"),
+            params,
+        )
+    }
+
+    pub fn delete_iteration(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/iterations/{id}")))
+    }
+
+    pub fn list_iteration_stories(
+        &self,
+        id: i64,
+        includes_description: Option<bool>,
+    ) -> Result<Vec<StorySlim>> {
+        let path = with_query(
+            &format!("/api/v3/iterations/{id}/stories"),
+            &[(
+                "includes_description",
+                includes_description.map(|b| b.to_string()),
+            )],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    /// Workspace-level toggle: turn the iterations feature off.
+    pub fn disable_iterations(&self) -> Result<()> {
+        send_empty(self.request(http::Method::Put, "/api/v3/iterations/disable"))
+    }
+
+    /// Workspace-level toggle: turn the iterations feature on.
+    pub fn enable_iterations(&self) -> Result<()> {
+        send_empty(self.request(http::Method::Put, "/api/v3/iterations/enable"))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Story
+//
+// Story carries cycle_time, lead_time, started_at, completed_at and the
+// owner/group/iteration/epic foreign keys: every tech-manager metric is
+// aggregation over these fields.  Several VCS sub-models (Branch, Commit,
+// PullRequest, Identity) come along because Story embeds them.
+//
+// Identity, LinkedFile and TypedStoryLink each have a wire field named
+// "type" (a Rust keyword); their FromJson/ToJson are hand-written and
+// expose the field as `kind` in Rust.
+//////////////////////////////////////////////////////////////////////////////
+
+pub struct Identity {
+    pub entity_type: String,
+    pub name: Option<String>,
+    pub kind: Option<String>,
+}
+
+impl ToJson for Identity {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("entity_type".into(), self.entity_type.to_json()),
+            ("name".into(), self.name.to_json()),
+            ("type".into(), self.kind.to_json()),
+        ])
+    }
+}
+
+impl FromJson for Identity {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut entity_type = None;
+        let mut name = None;
+        let mut kind = None;
+        for (k, val) in obj {
+            match k.as_str() {
+                "entity_type" => entity_type = Some(val),
+                "name" => name = Some(val),
+                "type" => kind = Some(val),
+                _ => {}
+            }
+        }
+        Ok(Identity {
+            entity_type: entity_type
+                .ok_or_else(|| jackson::Error::new("missing field: entity_type", 0))
+                .and_then(FromJson::from_json)?,
+            name: match name {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            kind: match kind {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+        })
+    }
+}
+
+json_struct! {
+    pub struct PullRequestLabel {
+        pub id: i64,
+        pub entity_type: String,
+        pub name: String,
+        pub color: String,
+        pub description: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct PullRequest {
+        pub id: i64,
+        pub entity_type: String,
+        pub url: String,
+        pub number: i64,
+        pub title: String,
+        pub branch_id: i64,
+        pub branch_name: String,
+        pub target_branch_id: i64,
+        pub target_branch_name: String,
+        pub repository_id: i64,
+        pub closed: bool,
+        pub merged: bool,
+        pub draft: bool,
+        pub has_overlapping_stories: bool,
+        pub overlapping_stories: Option<Vec<i64>>,
+        pub num_added: i64,
+        pub num_removed: i64,
+        pub num_modified: Option<i64>,
+        pub num_commits: Option<i64>,
+        pub vcs_labels: Option<Vec<PullRequestLabel>>,
+        pub review_status: Option<String>,
+        pub build_status: Option<String>,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+json_struct! {
+    pub struct Branch {
+        pub id: Option<i64>,
+        pub entity_type: String,
+        pub name: String,
+        pub url: String,
+        pub repository_id: i64,
+        pub deleted: bool,
+        pub persistent: bool,
+        pub merged_branch_ids: Vec<i64>,
+        pub pull_requests: Vec<PullRequest>,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct Commit {
+        pub id: Option<i64>,
+        pub entity_type: String,
+        pub url: String,
+        pub hash: String,
+        pub message: String,
+        pub author_id: Option<String>,
+        pub author_email: String,
+        pub author_identity: Identity,
+        pub repository_id: Option<i64>,
+        pub timestamp: String,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct LabelSlim {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: Option<String>,
+        pub color: Option<String>,
+        pub archived: bool,
+        pub external_id: Option<String>,
+        pub created_at: Option<String>,
+        pub updated_at: Option<String>,
+    }
+}
+
+pub struct LinkedFile {
+    pub id: i64,
+    pub entity_type: String,
+    pub name: String,
+    pub url: String,
+    pub kind: String,
+    pub description: Option<String>,
+    pub thumbnail_url: Option<String>,
+    pub size: Option<i64>,
+    pub content_type: Option<String>,
+    pub uploader_id: String,
+    pub story_ids: Vec<i64>,
+    pub mention_ids: Vec<String>,
+    pub member_mention_ids: Vec<String>,
+    pub group_mention_ids: Vec<String>,
+    pub created_at: String,
+    pub updated_at: String,
+}
+
+impl ToJson for LinkedFile {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("id".into(), self.id.to_json()),
+            ("entity_type".into(), self.entity_type.to_json()),
+            ("name".into(), self.name.to_json()),
+            ("url".into(), self.url.to_json()),
+            ("type".into(), self.kind.to_json()),
+            ("description".into(), self.description.to_json()),
+            ("thumbnail_url".into(), self.thumbnail_url.to_json()),
+            ("size".into(), self.size.to_json()),
+            ("content_type".into(), self.content_type.to_json()),
+            ("uploader_id".into(), self.uploader_id.to_json()),
+            ("story_ids".into(), self.story_ids.to_json()),
+            ("mention_ids".into(), self.mention_ids.to_json()),
+            (
+                "member_mention_ids".into(),
+                self.member_mention_ids.to_json(),
+            ),
+            ("group_mention_ids".into(), self.group_mention_ids.to_json()),
+            ("created_at".into(), self.created_at.to_json()),
+            ("updated_at".into(), self.updated_at.to_json()),
+        ])
+    }
+}
+
+impl FromJson for LinkedFile {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut fields: [Option<&Value>; 16] = [None; 16];
+        for (k, val) in obj {
+            let i = match k.as_str() {
+                "id" => 0,
+                "entity_type" => 1,
+                "name" => 2,
+                "url" => 3,
+                "type" => 4,
+                "description" => 5,
+                "thumbnail_url" => 6,
+                "size" => 7,
+                "content_type" => 8,
+                "uploader_id" => 9,
+                "story_ids" => 10,
+                "mention_ids" => 11,
+                "member_mention_ids" => 12,
+                "group_mention_ids" => 13,
+                "created_at" => 14,
+                "updated_at" => 15,
+                _ => continue,
+            };
+            fields[i] = Some(val);
+        }
+        macro_rules! req {
+            ($i:expr, $key:expr) => {
+                fields[$i]
+                    .ok_or_else(|| jackson::Error::new(concat!("missing field: ", $key), 0))
+                    .and_then(FromJson::from_json)?
+            };
+        }
+        Ok(LinkedFile {
+            id: req!(0, "id"),
+            entity_type: req!(1, "entity_type"),
+            name: req!(2, "name"),
+            url: req!(3, "url"),
+            kind: req!(4, "type"),
+            description: match fields[5] {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            thumbnail_url: match fields[6] {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            size: match fields[7] {
+                Some(v) => Option::<i64>::from_json(v)?,
+                None => None,
+            },
+            content_type: match fields[8] {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            uploader_id: req!(9, "uploader_id"),
+            story_ids: req!(10, "story_ids"),
+            mention_ids: req!(11, "mention_ids"),
+            member_mention_ids: req!(12, "member_mention_ids"),
+            group_mention_ids: req!(13, "group_mention_ids"),
+            created_at: req!(14, "created_at"),
+            updated_at: req!(15, "updated_at"),
+        })
+    }
+}
+
+json_struct! {
+    pub struct StoryReaction {
+        pub emoji: String,
+        pub permission_ids: Vec<String>,
+    }
+}
+
+json_struct! {
+    pub struct StoryComment {
+        pub id: i64,
+        pub app_url: String,
+        pub entity_type: String,
+        pub story_id: i64,
+        pub author_id: Option<String>,
+        pub text: Option<String>,
+        pub position: i64,
+        pub deleted: bool,
+        pub linked_to_slack: bool,
+        pub blocker: Option<bool>,
+        pub unblocks_parent: Option<bool>,
+        pub parent_id: Option<i64>,
+        pub external_id: Option<String>,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub reactions: Vec<StoryReaction>,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct StoryCustomField {
+        pub field_id: String,
+        pub value_id: String,
+        pub value: String,
+    }
+}
+
+json_struct! {
+    pub struct StoryStats {
+        pub num_related_documents: i64,
+    }
+}
+
+json_struct! {
+    pub struct SyncedItem {
+        pub external_id: String,
+        pub url: String,
+    }
+}
+
+pub struct TypedStoryLink {
+    pub id: i64,
+    pub entity_type: String,
+    pub kind: String,
+    pub verb: String,
+    pub subject_id: i64,
+    pub subject_workflow_state_id: i64,
+    pub object_id: i64,
+    pub created_at: String,
+    pub updated_at: String,
+}
+
+impl ToJson for TypedStoryLink {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("id".into(), self.id.to_json()),
+            ("entity_type".into(), self.entity_type.to_json()),
+            ("type".into(), self.kind.to_json()),
+            ("verb".into(), self.verb.to_json()),
+            ("subject_id".into(), self.subject_id.to_json()),
+            (
+                "subject_workflow_state_id".into(),
+                self.subject_workflow_state_id.to_json(),
+            ),
+            ("object_id".into(), self.object_id.to_json()),
+            ("created_at".into(), self.created_at.to_json()),
+            ("updated_at".into(), self.updated_at.to_json()),
+        ])
+    }
+}
+
+impl FromJson for TypedStoryLink {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut id = None;
+        let mut entity_type = None;
+        let mut kind = None;
+        let mut verb = None;
+        let mut subject_id = None;
+        let mut subject_workflow_state_id = None;
+        let mut object_id = None;
+        let mut created_at = None;
+        let mut updated_at = None;
+        for (k, val) in obj {
+            match k.as_str() {
+                "id" => id = Some(val),
+                "entity_type" => entity_type = Some(val),
+                "type" => kind = Some(val),
+                "verb" => verb = Some(val),
+                "subject_id" => subject_id = Some(val),
+                "subject_workflow_state_id" => subject_workflow_state_id = Some(val),
+                "object_id" => object_id = Some(val),
+                "created_at" => created_at = Some(val),
+                "updated_at" => updated_at = Some(val),
+                _ => {}
+            }
+        }
+        macro_rules! req {
+            ($opt:ident, $key:expr) => {
+                $opt.ok_or_else(|| jackson::Error::new(concat!("missing field: ", $key), 0))
+                    .and_then(FromJson::from_json)?
+            };
+        }
+        Ok(TypedStoryLink {
+            id: req!(id, "id"),
+            entity_type: req!(entity_type, "entity_type"),
+            kind: req!(kind, "type"),
+            verb: req!(verb, "verb"),
+            subject_id: req!(subject_id, "subject_id"),
+            subject_workflow_state_id: req!(subject_workflow_state_id, "subject_workflow_state_id"),
+            object_id: req!(object_id, "object_id"),
+            created_at: req!(created_at, "created_at"),
+            updated_at: req!(updated_at, "updated_at"),
+        })
+    }
+}
+
+json_struct! {
+    pub struct UploadedFile {
+        pub id: i64,
+        pub entity_type: String,
+        pub name: String,
+        pub filename: String,
+        pub url: Option<String>,
+        pub thumbnail_url: Option<String>,
+        pub description: Option<String>,
+        pub content_type: String,
+        pub size: i64,
+        pub uploader_id: String,
+        pub external_id: Option<String>,
+        pub story_ids: Vec<i64>,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct Story {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: String,
+        pub story_type: String,
+        pub workflow_id: i64,
+        pub workflow_state_id: i64,
+        pub epic_id: Option<i64>,
+        pub iteration_id: Option<i64>,
+        pub group_id: Option<String>,
+        pub project_id: Option<i64>,
+        pub parent_story_id: Option<i64>,
+        pub story_template_id: Option<String>,
+        pub external_id: Option<String>,
+        pub external_links: Vec<String>,
+        pub formatted_vcs_branch_name: Option<String>,
+        pub label_ids: Vec<i64>,
+        pub previous_iteration_ids: Vec<i64>,
+        pub sub_task_story_ids: Option<Vec<i64>>,
+        pub follower_ids: Vec<String>,
+        pub owner_ids: Vec<String>,
+        pub requested_by_id: String,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub estimate: Option<i64>,
+        pub position: i64,
+        pub deadline: Option<String>,
+        pub started: bool,
+        pub started_at: Option<String>,
+        pub started_at_override: Option<String>,
+        pub completed: bool,
+        pub completed_at: Option<String>,
+        pub completed_at_override: Option<String>,
+        pub blocker: bool,
+        pub blocked: bool,
+        pub archived: bool,
+        pub cycle_time: Option<i64>,
+        pub lead_time: Option<i64>,
+        pub moved_at: Option<String>,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+        pub stats: StoryStats,
+        pub synced_item: Option<SyncedItem>,
+        pub story_links: Vec<TypedStoryLink>,
+        pub labels: Vec<LabelSlim>,
+        pub custom_fields: Option<Vec<StoryCustomField>>,
+        pub linked_files: Vec<LinkedFile>,
+        pub comments: Vec<StoryComment>,
+        pub tasks: Vec<Task>,
+        pub branches: Vec<Branch>,
+        pub commits: Vec<Commit>,
+        pub pull_requests: Vec<PullRequest>,
+        pub files: Vec<UploadedFile>,
+    }
+}
+
+json_struct! {
+    pub struct StorySlim {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: Option<String>,
+        pub story_type: String,
+        pub workflow_id: i64,
+        pub workflow_state_id: i64,
+        pub epic_id: Option<i64>,
+        pub iteration_id: Option<i64>,
+        pub group_id: Option<String>,
+        pub project_id: Option<i64>,
+        pub parent_story_id: Option<i64>,
+        pub story_template_id: Option<String>,
+        pub external_id: Option<String>,
+        pub external_links: Vec<String>,
+        pub formatted_vcs_branch_name: Option<String>,
+        pub label_ids: Vec<i64>,
+        pub previous_iteration_ids: Vec<i64>,
+        pub sub_task_story_ids: Option<Vec<i64>>,
+        pub follower_ids: Vec<String>,
+        pub owner_ids: Vec<String>,
+        pub requested_by_id: String,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub estimate: Option<i64>,
+        pub position: i64,
+        pub deadline: Option<String>,
+        pub started: bool,
+        pub started_at: Option<String>,
+        pub started_at_override: Option<String>,
+        pub completed: bool,
+        pub completed_at: Option<String>,
+        pub completed_at_override: Option<String>,
+        pub blocker: bool,
+        pub blocked: bool,
+        pub archived: bool,
+        pub cycle_time: Option<i64>,
+        pub lead_time: Option<i64>,
+        pub moved_at: Option<String>,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+        pub task_ids: Vec<i64>,
+        pub num_tasks_completed: i64,
+        pub comment_ids: Vec<i64>,
+        pub file_ids: Vec<i64>,
+        pub linked_file_ids: Vec<i64>,
+        pub stats: StoryStats,
+        pub synced_item: Option<SyncedItem>,
+        pub story_links: Vec<TypedStoryLink>,
+        pub labels: Vec<LabelSlim>,
+        pub custom_fields: Option<Vec<StoryCustomField>>,
+    }
+}
+
+params_struct! {
+    pub struct CreateLabelParams {
+        required name: String,
+        opt description: String,
+        opt color: String,
+        opt external_id: String,
+    }
+}
+
+params_struct! {
+    pub struct CreateStoryCommentParams {
+        required text: String,
+        opt author_id: String,
+        opt parent_id: i64,
+        opt external_id: String,
+        opt created_at: String,
+        opt updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct CreateStoryLinkParams {
+        required verb: String,
+        opt subject_id: i64,
+        opt object_id: i64,
+    }
+}
+
+params_struct! {
+    pub struct CustomFieldValueParams {
+        required field_id: String,
+        required value_id: String,
+        opt value: String,
+    }
+}
+
+params_struct! {
+    pub struct LinkSubTaskParams {
+        required story_id: i64,
+    }
+}
+
+params_struct! {
+    pub struct CreateStoryParams {
+        required name: String,
+        opt description: String,
+        opt story_type: String,
+        opt workflow_state_id: i64,
+        opt project_id: i64,
+        opt epic_id: i64,
+        opt iteration_id: i64,
+        opt group_id: String,
+        opt parent_story_id: i64,
+        opt story_template_id: String,
+        opt requested_by_id: String,
+        opt owner_ids: Vec<String>,
+        opt follower_ids: Vec<String>,
+        opt label_ids: Vec<i64>,
+        opt labels: Vec<CreateLabelParams>,
+        opt comments: Vec<CreateStoryCommentParams>,
+        opt tasks: Vec<CreateTaskParams>,
+        opt story_links: Vec<CreateStoryLinkParams>,
+        opt custom_fields: Vec<CustomFieldValueParams>,
+        opt external_id: String,
+        opt external_links: Vec<String>,
+        opt file_ids: Vec<i64>,
+        opt linked_file_ids: Vec<i64>,
+        opt source_task_id: i64,
+        opt move_to: String,
+        opt archived: bool,
+        opt estimate: i64,
+        opt deadline: String,
+        opt started_at_override: String,
+        opt completed_at_override: String,
+        opt created_at: String,
+        opt updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateStoryParams {
+        opt name: String,
+        opt description: String,
+        opt story_type: String,
+        opt workflow_state_id: i64,
+        opt project_id: i64,
+        opt epic_id: i64,
+        opt iteration_id: i64,
+        opt group_id: String,
+        opt parent_story_id: i64,
+        opt requested_by_id: String,
+        opt owner_ids: Vec<String>,
+        opt follower_ids: Vec<String>,
+        opt label_ids: Vec<i64>,
+        opt labels: Vec<CreateLabelParams>,
+        opt branch_ids: Vec<i64>,
+        opt commit_ids: Vec<i64>,
+        opt pull_request_ids: Vec<i64>,
+        opt sub_tasks: Vec<LinkSubTaskParams>,
+        opt custom_fields: Vec<CustomFieldValueParams>,
+        opt external_links: Vec<String>,
+        opt file_ids: Vec<i64>,
+        opt linked_file_ids: Vec<i64>,
+        opt move_to: String,
+        opt before_id: i64,
+        opt after_id: i64,
+        opt archived: bool,
+        opt estimate: i64,
+        opt deadline: String,
+        opt started_at_override: String,
+        opt completed_at_override: String,
+    }
+}
+
+impl Client {
+    pub fn get_story(&self, id: i64) -> Result<Story> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/stories/{id}")))
+    }
+
+    pub fn create_story(&self, params: &CreateStoryParams) -> Result<Story> {
+        self.json_call(http::Method::Post, "/api/v3/stories", params)
+    }
+
+    pub fn update_story(&self, id: i64, params: &UpdateStoryParams) -> Result<Story> {
+        self.json_call(http::Method::Put, &format!("/api/v3/stories/{id}"), params)
+    }
+
+    pub fn delete_story(&self, id: i64) -> Result<()> {
+        send_empty(self.request(http::Method::Delete, &format!("/api/v3/stories/{id}")))
+    }
+
+    pub fn list_group_stories(
+        &self,
+        group_id: &str,
+        limit: Option<i64>,
+        offset: Option<i64>,
+    ) -> Result<Vec<StorySlim>> {
+        let path = with_query(
+            &format!("/api/v3/groups/{group_id}/stories"),
+            &[
+                ("limit", limit.map(|n| n.to_string())),
+                ("offset", offset.map(|n| n.to_string())),
+            ],
+        );
+        send_json(self.request(http::Method::Get, &path))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Search
+//
+// Cursor-paginated search.  Shortcut returns each page with `next` set to
+// an opaque cursor; pass it back via SearchQuery::next to fetch the next
+// page.  SearchQuery is shared by both story and iteration search and is
+// reusable for the future search endpoints (epics, objectives, etc.).
+//
+// StorySearchResult mirrors Story's payload but loosens optionality on
+// the bulky nested arrays (Shortcut omits them when `detail=slim`) and
+// adds the slim id-collection fields (task_ids, comment_ids, ...).  Kept
+// as its own type rather than retrofitting Story to dodge decode errors
+// on slim responses.
+//////////////////////////////////////////////////////////////////////////////
+
+pub struct SearchQuery {
+    pub query: String,
+    pub page_size: Option<i64>,
+    pub detail: Option<String>,
+    pub next: Option<String>,
+    pub entity_types: Option<Vec<String>>,
+}
+
+impl SearchQuery {
+    pub fn new(query: impl Into<String>) -> Self {
+        Self {
+            query: query.into(),
+            page_size: None,
+            detail: None,
+            next: None,
+            entity_types: None,
+        }
+    }
+
+    pub fn page_size(mut self, n: i64) -> Self {
+        self.page_size = Some(n);
+        self
+    }
+
+    pub fn detail(mut self, s: impl Into<String>) -> Self {
+        self.detail = Some(s.into());
+        self
+    }
+
+    pub fn next(mut self, s: impl Into<String>) -> Self {
+        self.next = Some(s.into());
+        self
+    }
+
+    pub fn entity_types(mut self, v: Vec<String>) -> Self {
+        self.entity_types = Some(v);
+        self
+    }
+
+    fn append_to(&self, path: &str) -> String {
+        with_query(
+            path,
+            &[
+                ("query", Some(self.query.clone())),
+                ("page_size", self.page_size.map(|n| n.to_string())),
+                ("detail", self.detail.clone()),
+                ("next", self.next.clone()),
+                (
+                    "entity_types",
+                    self.entity_types.as_ref().map(|v| v.join(",")),
+                ),
+            ],
+        )
+    }
+}
+
+json_struct! {
+    pub struct StorySearchResult {
+        pub id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub app_url: String,
+        pub name: String,
+        pub description: Option<String>,
+        pub story_type: String,
+        pub workflow_id: i64,
+        pub workflow_state_id: i64,
+        pub epic_id: Option<i64>,
+        pub iteration_id: Option<i64>,
+        pub group_id: Option<String>,
+        pub project_id: Option<i64>,
+        pub parent_story_id: Option<i64>,
+        pub story_template_id: Option<String>,
+        pub external_id: Option<String>,
+        pub external_links: Vec<String>,
+        pub formatted_vcs_branch_name: Option<String>,
+        pub label_ids: Vec<i64>,
+        pub previous_iteration_ids: Vec<i64>,
+        pub sub_task_story_ids: Option<Vec<i64>>,
+        pub follower_ids: Vec<String>,
+        pub owner_ids: Vec<String>,
+        pub requested_by_id: String,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub estimate: Option<i64>,
+        pub position: i64,
+        pub deadline: Option<String>,
+        pub started: bool,
+        pub started_at: Option<String>,
+        pub started_at_override: Option<String>,
+        pub completed: bool,
+        pub completed_at: Option<String>,
+        pub completed_at_override: Option<String>,
+        pub blocker: bool,
+        pub blocked: bool,
+        pub archived: bool,
+        pub cycle_time: Option<i64>,
+        pub lead_time: Option<i64>,
+        pub moved_at: Option<String>,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+        pub task_ids: Option<Vec<i64>>,
+        pub num_tasks_completed: Option<i64>,
+        pub comment_ids: Option<Vec<i64>>,
+        pub file_ids: Option<Vec<i64>>,
+        pub linked_file_ids: Option<Vec<i64>>,
+        pub stats: StoryStats,
+        pub synced_item: Option<SyncedItem>,
+        pub story_links: Vec<TypedStoryLink>,
+        pub labels: Vec<LabelSlim>,
+        pub custom_fields: Option<Vec<StoryCustomField>>,
+        pub linked_files: Option<Vec<LinkedFile>>,
+        pub comments: Option<Vec<StoryComment>>,
+        pub tasks: Option<Vec<Task>>,
+        pub branches: Option<Vec<Branch>>,
+        pub commits: Option<Vec<Commit>>,
+        pub pull_requests: Option<Vec<PullRequest>>,
+        pub files: Option<Vec<UploadedFile>>,
+    }
+}
+
+json_struct! {
+    pub struct StorySearchResults {
+        pub total: i64,
+        pub data: Vec<StorySearchResult>,
+        pub next: Option<String>,
+    }
+}
+
+json_struct! {
+    pub struct IterationSearchResults {
+        pub total: i64,
+        pub data: Vec<IterationSlim>,
+        pub next: Option<String>,
+    }
+}
+
+impl Client {
+    pub fn search_stories(&self, q: &SearchQuery) -> Result<StorySearchResults> {
+        let path = q.append_to("/api/v3/search/stories");
+        send_json(self.request(http::Method::Get, &path))
+    }
+
+    pub fn search_iterations(&self, q: &SearchQuery) -> Result<IterationSearchResults> {
+        let path = q.append_to("/api/v3/search/iterations");
+        send_json(self.request(http::Method::Get, &path))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Story comment endpoints
+//
+// StoryComment and StoryReaction structs live in the Story section;
+// CreateStoryCommentParams was defined alongside CreateStoryParams (it
+// appears nested in Vec<CreateStoryCommentParams> when creating a story
+// with seed comments).  This commit fills in the comment CRUD plus
+// reactions and the slack-unlink endpoint.
+//////////////////////////////////////////////////////////////////////////////
+
+params_struct! {
+    pub struct UpdateStoryCommentParams {
+        required text: String,
+    }
+}
+
+params_struct! {
+    pub struct CreateOrDeleteReactionParams {
+        required emoji: String,
+    }
+}
+
+impl Client {
+    pub fn list_story_comments(&self, story_id: i64) -> Result<Vec<StoryComment>> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/stories/{story_id}/comments"),
+        ))
+    }
+
+    pub fn create_story_comment(
+        &self,
+        story_id: i64,
+        params: &CreateStoryCommentParams,
+    ) -> Result<StoryComment> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/stories/{story_id}/comments"),
+            params,
+        )
+    }
+
+    pub fn get_story_comment(&self, story_id: i64, comment_id: i64) -> Result<StoryComment> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/stories/{story_id}/comments/{comment_id}"),
+        ))
+    }
+
+    pub fn update_story_comment(
+        &self,
+        story_id: i64,
+        comment_id: i64,
+        params: &UpdateStoryCommentParams,
+    ) -> Result<StoryComment> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/stories/{story_id}/comments/{comment_id}"),
+            params,
+        )
+    }
+
+    pub fn delete_story_comment(&self, story_id: i64, comment_id: i64) -> Result<()> {
+        send_empty(self.request(
+            http::Method::Delete,
+            &format!("/api/v3/stories/{story_id}/comments/{comment_id}"),
+        ))
+    }
+
+    pub fn create_story_reaction(
+        &self,
+        story_id: i64,
+        comment_id: i64,
+        params: &CreateOrDeleteReactionParams,
+    ) -> Result<Vec<StoryReaction>> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/stories/{story_id}/comments/{comment_id}/reactions"),
+            params,
+        )
+    }
+
+    pub fn delete_story_reaction(
+        &self,
+        story_id: i64,
+        comment_id: i64,
+        params: &CreateOrDeleteReactionParams,
+    ) -> Result<()> {
+        let json = params
+            .to_json()
+            .stringify()
+            .map_err(|e| Error::Decode(e.to_string()))?;
+        let rb = self
+            .request(
+                http::Method::Delete,
+                &format!("/api/v3/stories/{story_id}/comments/{comment_id}/reactions"),
+            )
+            .header("Content-Type", "application/json")
+            .body(json);
+        send_empty(rb)
+    }
+
+    pub fn unlink_comment_thread_from_slack(
+        &self,
+        story_id: i64,
+        comment_id: i64,
+    ) -> Result<StoryComment> {
+        send_json(self.request(
+            http::Method::Post,
+            &format!("/api/v3/stories/{story_id}/comments/{comment_id}/unlink-from-slack"),
+        ))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Epic comment endpoints
+//
+// ThreadedComment struct lives near Epic.  Epic comments are threaded
+// (a comment can have child comments via the same ThreadedComment
+// payload), so the "reply" endpoint POSTs into a parent comment.
+// CreateEpicCommentParams covers both the top-level create and the
+// reply create -- the wire shape is identical.
+//////////////////////////////////////////////////////////////////////////////
+
+params_struct! {
+    pub struct CreateEpicCommentParams {
+        required text: String,
+        opt author_id: String,
+        opt parent_id: i64,
+        opt external_id: String,
+        opt created_at: String,
+        opt updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateEpicCommentParams {
+        required text: String,
+    }
+}
+
+impl Client {
+    pub fn list_epic_comments(&self, epic_id: i64) -> Result<Vec<ThreadedComment>> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/epics/{epic_id}/comments"),
+        ))
+    }
+
+    pub fn create_epic_comment(
+        &self,
+        epic_id: i64,
+        params: &CreateEpicCommentParams,
+    ) -> Result<ThreadedComment> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/epics/{epic_id}/comments"),
+            params,
+        )
+    }
+
+    pub fn create_epic_comment_reply(
+        &self,
+        epic_id: i64,
+        comment_id: i64,
+        params: &CreateEpicCommentParams,
+    ) -> Result<ThreadedComment> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/epics/{epic_id}/comments/{comment_id}"),
+            params,
+        )
+    }
+
+    pub fn get_epic_comment(&self, epic_id: i64, comment_id: i64) -> Result<ThreadedComment> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/epics/{epic_id}/comments/{comment_id}"),
+        ))
+    }
+
+    pub fn update_epic_comment(
+        &self,
+        epic_id: i64,
+        comment_id: i64,
+        params: &UpdateEpicCommentParams,
+    ) -> Result<ThreadedComment> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/epics/{epic_id}/comments/{comment_id}"),
+            params,
+        )
+    }
+
+    pub fn delete_epic_comment(&self, epic_id: i64, comment_id: i64) -> Result<()> {
+        send_empty(self.request(
+            http::Method::Delete,
+            &format!("/api/v3/epics/{epic_id}/comments/{comment_id}"),
+        ))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Task - sub-resource of Story
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Task {
+        pub id: i64,
+        pub story_id: i64,
+        pub global_id: String,
+        pub entity_type: String,
+        pub description: String,
+        pub complete: bool,
+        pub completed_at: Option<String>,
+        pub position: i64,
+        pub mention_ids: Vec<String>,
+        pub member_mention_ids: Vec<String>,
+        pub group_mention_ids: Vec<String>,
+        pub owner_ids: Vec<String>,
+        pub external_id: Option<String>,
+        pub created_at: String,
+        pub updated_at: Option<String>,
+    }
+}
+
+params_struct! {
+    pub struct CreateTaskParams {
+        required description: String,
+        opt complete: bool,
+        opt owner_ids: Vec<String>,
+        opt external_id: String,
+        opt created_at: String,
+        opt updated_at: String,
+    }
+}
+
+params_struct! {
+    pub struct UpdateTaskParams {
+        opt description: String,
+        opt owner_ids: Vec<String>,
+        opt complete: bool,
+        opt before_id: i64,
+        opt after_id: i64,
+    }
+}
+
+impl Client {
+    pub fn list_tasks(&self, story_id: i64) -> Result<Vec<Task>> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/stories/{story_id}/tasks"),
+        ))
+    }
+
+    pub fn get_task(&self, story_id: i64, task_id: i64) -> Result<Task> {
+        send_json(self.request(
+            http::Method::Get,
+            &format!("/api/v3/stories/{story_id}/tasks/{task_id}"),
+        ))
+    }
+
+    pub fn create_task(&self, story_id: i64, params: &CreateTaskParams) -> Result<Task> {
+        self.json_call(
+            http::Method::Post,
+            &format!("/api/v3/stories/{story_id}/tasks"),
+            params,
+        )
+    }
+
+    pub fn update_task(
+        &self,
+        story_id: i64,
+        task_id: i64,
+        params: &UpdateTaskParams,
+    ) -> Result<Task> {
+        self.json_call(
+            http::Method::Put,
+            &format!("/api/v3/stories/{story_id}/tasks/{task_id}"),
+            params,
+        )
+    }
+
+    pub fn delete_task(&self, story_id: i64, task_id: i64) -> Result<()> {
+        send_empty(self.request(
+            http::Method::Delete,
+            &format!("/api/v3/stories/{story_id}/tasks/{task_id}"),
+        ))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Workflow
+//
+// `WorkflowState.type` collides with a Rust keyword.  `stringify!(r#type)`
+// expands to the string `"r#type"`, so the json_struct! macro cannot map
+// it.  FromJson is hand-written here; the Rust field is named `kind`
+// while the wire field stays `"type"`.
+//////////////////////////////////////////////////////////////////////////////
+
+json_struct! {
+    pub struct Workflow {
+        pub id: i64,
+        pub entity_type: String,
+        pub name: String,
+        pub description: String,
+        pub team_id: i64,
+        pub default_state_id: i64,
+        pub project_ids: Vec<f64>,
+        pub auto_assign_owner: bool,
+        pub states: Vec<WorkflowState>,
+        pub created_at: String,
+        pub updated_at: String,
+    }
+}
+
+pub struct WorkflowState {
+    pub id: i64,
+    pub global_id: String,
+    pub entity_type: String,
+    pub name: String,
+    pub description: String,
+    pub color: Option<String>,
+    pub verb: Option<String>,
+    pub kind: String,
+    pub position: i64,
+    pub num_stories: i64,
+    pub num_story_templates: i64,
+    pub created_at: String,
+    pub updated_at: String,
+}
+
+impl ToJson for WorkflowState {
+    fn to_json(&self) -> Value {
+        Value::Object(vec![
+            ("id".into(), self.id.to_json()),
+            ("global_id".into(), self.global_id.to_json()),
+            ("entity_type".into(), self.entity_type.to_json()),
+            ("name".into(), self.name.to_json()),
+            ("description".into(), self.description.to_json()),
+            ("color".into(), self.color.to_json()),
+            ("verb".into(), self.verb.to_json()),
+            ("type".into(), self.kind.to_json()),
+            ("position".into(), self.position.to_json()),
+            ("num_stories".into(), self.num_stories.to_json()),
+            (
+                "num_story_templates".into(),
+                self.num_story_templates.to_json(),
+            ),
+            ("created_at".into(), self.created_at.to_json()),
+            ("updated_at".into(), self.updated_at.to_json()),
+        ])
+    }
+}
+
+impl FromJson for WorkflowState {
+    fn from_json(v: &Value) -> std::result::Result<Self, jackson::Error> {
+        let obj = v
+            .as_object()
+            .ok_or_else(|| jackson::Error::new("expected object", 0))?;
+        let mut id = None;
+        let mut global_id = None;
+        let mut entity_type = None;
+        let mut name = None;
+        let mut description = None;
+        let mut color = None;
+        let mut verb = None;
+        let mut kind = None;
+        let mut position = None;
+        let mut num_stories = None;
+        let mut num_story_templates = None;
+        let mut created_at = None;
+        let mut updated_at = None;
+        for (k, val) in obj {
+            match k.as_str() {
+                "id" => id = Some(val),
+                "global_id" => global_id = Some(val),
+                "entity_type" => entity_type = Some(val),
+                "name" => name = Some(val),
+                "description" => description = Some(val),
+                "color" => color = Some(val),
+                "verb" => verb = Some(val),
+                "type" => kind = Some(val),
+                "position" => position = Some(val),
+                "num_stories" => num_stories = Some(val),
+                "num_story_templates" => num_story_templates = Some(val),
+                "created_at" => created_at = Some(val),
+                "updated_at" => updated_at = Some(val),
+                _ => {}
+            }
+        }
+        macro_rules! req {
+            ($opt:ident, $key:expr) => {
+                $opt.ok_or_else(|| jackson::Error::new(concat!("missing field: ", $key), 0))
+                    .and_then(FromJson::from_json)?
+            };
+        }
+        Ok(WorkflowState {
+            id: req!(id, "id"),
+            global_id: req!(global_id, "global_id"),
+            entity_type: req!(entity_type, "entity_type"),
+            name: req!(name, "name"),
+            description: req!(description, "description"),
+            color: match color {
+                Some(v) => Option::<String>::from_json(v)?,
+                None => None,
+            },
+            verb: req!(verb, "verb"),
+            kind: req!(kind, "type"),
+            position: req!(position, "position"),
+            num_stories: req!(num_stories, "num_stories"),
+            num_story_templates: req!(num_story_templates, "num_story_templates"),
+            created_at: req!(created_at, "created_at"),
+            updated_at: req!(updated_at, "updated_at"),
+        })
+    }
+}
+
+impl Client {
+    pub fn list_workflows(&self) -> Result<Vec<Workflow>> {
+        send_json(self.request(http::Method::Get, "/api/v3/workflows"))
+    }
+
+    pub fn get_workflow(&self, id: i64) -> Result<Workflow> {
+        send_json(self.request(http::Method::Get, &format!("/api/v3/workflows/{id}")))
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Tests
+//////////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn version_is_set() {
+        assert!(!version().is_empty());
+    }
+
+    #[test]
+    fn params_default_is_empty_when_only_opts() {
+        let p = UpdateTaskParams::default();
+        assert_eq!(p.to_json().stringify().unwrap(), "{}");
+    }
+
+    #[test]
+    fn params_required_always_emitted() {
+        let p = CreateTaskParams::default();
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"description":""}"#);
+    }
+
+    #[test]
+    fn params_opts_only_emitted_when_set() {
+        let p = CreateTaskParams::default()
+            .description("write tests")
+            .complete(true);
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"description":"write tests","complete":true}"#);
+    }
+
+    #[test]
+    fn params_opt_vec_round_trip() {
+        let p = UpdateTaskParams::default()
+            .owner_ids(vec!["abc".to_string(), "def".to_string()])
+            .complete(false);
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"owner_ids":["abc","def"],"complete":false}"#);
+    }
+
+    #[test]
+    fn params_field_access_still_works() {
+        let mut p = CreateTaskParams::default();
+        p.description = "direct".into();
+        p.complete = Some(true);
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"description":"direct","complete":true}"#);
+    }
+
+    #[test]
+    fn task_round_trip() {
+        let raw = r#"{
+            "id": 42, "story_id": 7, "global_id": "v3:task:42",
+            "entity_type": "story-task", "description": "do it",
+            "complete": false, "completed_at": null, "position": 1,
+            "mention_ids": [], "member_mention_ids": [],
+            "group_mention_ids": [], "owner_ids": ["u1"],
+            "external_id": null, "created_at": "2026-04-25T00:00:00Z",
+            "updated_at": null
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let t = Task::from_json(&v).unwrap();
+        assert_eq!(t.id, 42);
+        assert_eq!(t.story_id, 7);
+        assert_eq!(t.description, "do it");
+        assert_eq!(t.owner_ids, vec!["u1".to_string()]);
+        assert!(t.completed_at.is_none());
+    }
+
+    #[test]
+    fn client_construction() {
+        let c = Client::new("xxx");
+        assert_eq!(c.token, "xxx");
+        assert_eq!(c.base, BASE_URL);
+        let c = Client::with_base("yyy", "http://localhost:8080");
+        assert_eq!(c.base, "http://localhost:8080");
+    }
+
+    #[test]
+    fn error_display() {
+        let e = Error::Status {
+            code: 422,
+            body: "{\"message\":\"bad\"}".into(),
+        };
+        let s = format!("{e}");
+        assert!(s.contains("422"));
+        assert!(s.contains("bad"));
+    }
+
+    #[test]
+    fn with_query_skips_none_and_encodes() {
+        let p = with_query(
+            "/api/v3/members",
+            &[
+                ("org-public-id", Some("a b/c".to_string())),
+                ("disabled", None),
+            ],
+        );
+        assert_eq!(p, "/api/v3/members?org-public-id=a%20b%2Fc");
+        let p = with_query("/api/v3/members", &[("disabled", Some("true".into()))]);
+        assert_eq!(p, "/api/v3/members?disabled=true");
+        let p = with_query(
+            "/api/v3/groups",
+            &[("archived", None::<String>.map(|s: String| s))],
+        );
+        assert_eq!(p, "/api/v3/groups");
+    }
+
+    #[test]
+    fn member_round_trip() {
+        let raw = r#"{
+            "id": "12345678-1234-1234-1234-123456789abc",
+            "global_id": "v3:member:abc",
+            "entity_type": "member",
+            "role": "admin",
+            "state": "full",
+            "disabled": false,
+            "created_without_invite": false,
+            "group_ids": ["g1"],
+            "installation_id": null,
+            "replaced_by": null,
+            "profile": {
+                "id": "12345678-1234-1234-1234-123456789def",
+                "entity_type": "profile",
+                "name": "Murilo",
+                "mention_name": "ijanc",
+                "email_address": "x@y.z",
+                "gravatar_hash": null,
+                "display_icon": null,
+                "deactivated": false,
+                "is_owner": true
+            },
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": null
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let m = Member::from_json(&v).unwrap();
+        assert_eq!(m.role, "admin");
+        assert!(m.profile.is_owner);
+        assert_eq!(m.profile.mention_name, "ijanc");
+        assert!(m.profile.display_icon.is_none());
+    }
+
+    #[test]
+    fn group_round_trip() {
+        let raw = r##"{
+            "id": "12345678-1234-1234-1234-aaaaaaaaaaaa",
+            "global_id": "v3:group:1",
+            "entity_type": "group",
+            "app_url": "https://app.shortcut.com/x/team/1",
+            "name": "Platform",
+            "mention_name": "platform",
+            "description": "core infra",
+            "archived": false,
+            "color": "#112233",
+            "color_key": "blue",
+            "display_icon": null,
+            "default_workflow_id": 500,
+            "member_ids": ["m1", "m2"],
+            "workflow_ids": [500, 501],
+            "num_stories": 42,
+            "num_stories_started": 5,
+            "num_stories_backlog": 10,
+            "num_epics_started": 1,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-04-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let g = Group::from_json(&v).unwrap();
+        assert_eq!(g.name, "Platform");
+        assert_eq!(g.member_ids.len(), 2);
+        assert_eq!(g.default_workflow_id, Some(500));
+    }
+
+    #[test]
+    fn create_group_params_emits_required_only() {
+        let p = CreateGroupParams::default()
+            .name("Platform")
+            .mention_name("platform");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"name":"Platform","mention_name":"platform"}"#);
+    }
+
+    #[test]
+    fn workflow_state_decodes_type_to_kind() {
+        let raw = r##"{
+            "id": 500001,
+            "global_id": "v3:state:500001",
+            "entity_type": "workflow-state",
+            "name": "In Progress",
+            "description": "",
+            "color": "#abcdef",
+            "verb": "started",
+            "type": "started",
+            "position": 200,
+            "num_stories": 3,
+            "num_story_templates": 0,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let s = WorkflowState::from_json(&v).unwrap();
+        assert_eq!(s.kind, "started");
+        assert_eq!(s.name, "In Progress");
+        assert_eq!(s.color.as_deref(), Some("#abcdef"));
+    }
+
+    #[test]
+    fn workflow_state_missing_type_errors() {
+        let raw = r#"{
+            "id": 1, "global_id": "v3:state:1", "entity_type": "workflow-state",
+            "name": "x", "description": "", "color": null, "verb": null,
+            "position": 0, "num_stories": 0, "num_story_templates": 0,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let e = WorkflowState::from_json(&v).err().unwrap();
+        assert_eq!(e.message(), "missing field: type");
+    }
+
+    #[test]
+    fn typed_story_link_renames_type_to_kind() {
+        let raw = r##"{
+            "id": 9, "entity_type": "story-link", "type": "object",
+            "verb": "blocks", "subject_id": 1, "subject_workflow_state_id": 500,
+            "object_id": 2,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let l = TypedStoryLink::from_json(&v).unwrap();
+        assert_eq!(l.kind, "object");
+        assert_eq!(l.verb, "blocks");
+    }
+
+    #[test]
+    fn linked_file_renames_type_and_handles_optionals() {
+        let raw = r##"{
+            "id": 7, "entity_type": "linked-file", "name": "PRD.pdf",
+            "url": "https://drive.google.com/x", "type": "google",
+            "description": null, "thumbnail_url": null, "size": null,
+            "content_type": null, "uploader_id": "abc",
+            "story_ids": [1, 2], "mention_ids": [],
+            "member_mention_ids": [], "group_mention_ids": [],
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let f = LinkedFile::from_json(&v).unwrap();
+        assert_eq!(f.kind, "google");
+        assert_eq!(f.story_ids, vec![1, 2]);
+        assert!(f.size.is_none());
+    }
+
+    #[test]
+    fn identity_renames_type() {
+        let raw = r#"{"entity_type":"identity","name":"Murilo","type":"github"}"#;
+        let v: Value = raw.parse().unwrap();
+        let i = Identity::from_json(&v).unwrap();
+        assert_eq!(i.kind.as_deref(), Some("github"));
+    }
+
+    #[test]
+    fn upload_files_rejects_empty_and_too_many() {
+        let c = Client::new("xxx");
+        let many: Vec<FileUpload> = (0..5)
+            .map(|_| FileUpload::new("/etc/hostname", "text/plain"))
+            .collect();
+        assert!(matches!(c.upload_files(&[], None), Err(Error::Decode(_))));
+        assert!(matches!(c.upload_files(&many, None), Err(Error::Decode(_))));
+    }
+
+    #[test]
+    fn create_linked_file_params_emit_type_as_kind() {
+        let p = CreateLinkedFileParams::new("PRD.pdf", "https://drive.google.com/x", "google")
+            .story_id(123)
+            .content_type("application/pdf");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(
+            s,
+            r#"{"name":"PRD.pdf","url":"https://drive.google.com/x","type":"google","story_id":123,"content_type":"application/pdf"}"#
+        );
+    }
+
+    #[test]
+    fn update_linked_file_params_skip_unset() {
+        let p = UpdateLinkedFileParams::default().description("renamed");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"description":"renamed"}"#);
+    }
+
+    #[test]
+    fn doc_round_trip() {
+        let raw = r##"{
+            "id": "uuid-doc", "title": "Q2 PRD",
+            "content_markdown": "# Goals\n- Ship X",
+            "content_html": "<h1>Goals</h1>",
+            "app_url": "https://x", "archived": false,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-04-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let d = Doc::from_json(&v).unwrap();
+        assert_eq!(d.title.as_deref(), Some("Q2 PRD"));
+        assert!(d.content_markdown.is_some());
+    }
+
+    #[test]
+    fn create_doc_params_required() {
+        let p = CreateDocParams::default()
+            .title("ADR-001 storage")
+            .content("# context\n");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(
+            s,
+            r##"{"title":"ADR-001 storage","content":"# context\n"}"##
+        );
+    }
+
+    #[test]
+    fn health_params_emit_required_status() {
+        let p = CreateHealthParams::default()
+            .status("on track")
+            .text("nothing blocking");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"status":"on track","text":"nothing blocking"}"#);
+    }
+
+    #[test]
+    fn health_round_trip() {
+        let raw = r#"{
+            "id": "uuid-h", "entity_type": "health",
+            "status": "on track", "text": "shipping on time",
+            "author_id": "u1", "epic_id": 7, "objective_id": null,
+            "created_at": "2026-04-20T00:00:00Z",
+            "updated_at": "2026-04-20T00:00:00Z"
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let h = Health::from_json(&v).unwrap();
+        assert_eq!(h.status, "on track");
+        assert_eq!(h.epic_id, Some(7));
+        assert!(h.objective_id.is_none());
+    }
+
+    #[test]
+    fn category_renames_type_to_kind() {
+        let raw = r##"{
+            "id": 5, "global_id": "v3:cat:5", "entity_type": "category",
+            "name": "Q2 OKRs", "type": "objective", "color": "#0044ff",
+            "external_id": null, "archived": false,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-15T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let c = Category::from_json(&v).unwrap();
+        assert_eq!(c.kind, "objective");
+        assert_eq!(c.name, "Q2 OKRs");
+    }
+
+    #[test]
+    fn key_result_renames_type_and_decodes_values() {
+        let raw = r#"{
+            "id": "uuid-kr",
+            "name": "p99 latency under 200ms",
+            "objective_id": 42,
+            "type": "numeric",
+            "progress": 60,
+            "initial_observed_value": {"numeric_value": "350"},
+            "current_observed_value": {"numeric_value": "230"},
+            "current_target_value": {"numeric_value": "200"}
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let kr = KeyResult::from_json(&v).unwrap();
+        assert_eq!(kr.kind, "numeric");
+        assert_eq!(kr.progress, 60);
+        assert_eq!(
+            kr.current_observed_value.numeric_value.as_deref(),
+            Some("230")
+        );
+    }
+
+    #[test]
+    fn objective_round_trip_with_categories() {
+        let raw = r#"{
+            "id": 7, "global_id": "v3:obj:7", "entity_type": "objective",
+            "app_url": "https://x", "name": "Reliability",
+            "description": "", "state": "in progress",
+            "categories": [],
+            "key_result_ids": ["uuid-1"],
+            "position": 1, "started": true,
+            "started_at": "2026-01-01T00:00:00Z",
+            "started_at_override": null,
+            "completed": false, "completed_at": null,
+            "completed_at_override": null,
+            "archived": false,
+            "stats": {
+                "num_related_documents": 3,
+                "average_cycle_time": 100000,
+                "average_lead_time": 200000
+            },
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-04-01T00:00:00Z"
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let o = Objective::from_json(&v).unwrap();
+        assert_eq!(o.name, "Reliability");
+        assert_eq!(o.key_result_ids, vec!["uuid-1".to_string()]);
+        assert_eq!(o.stats.average_cycle_time, Some(100000));
+    }
+
+    #[test]
+    fn epic_state_renames_type_to_kind() {
+        let raw = r##"{
+            "id": 500001, "global_id": "v3:epic-state:1",
+            "entity_type": "epic-workflow-state", "name": "Done",
+            "description": "", "color": "#00ff00", "type": "done",
+            "position": 300,
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let s = EpicState::from_json(&v).unwrap();
+        assert_eq!(s.kind, "done");
+        assert_eq!(s.position, 300);
+    }
+
+    #[test]
+    fn threaded_comment_recurses() {
+        let raw = r#"{
+            "id": 1, "app_url": "https://x", "entity_type": "epic-comment",
+            "text": "outer", "author_id": "a", "deleted": false,
+            "mention_ids": [], "member_mention_ids": [],
+            "group_mention_ids": [], "external_id": null,
+            "comments": [{
+                "id": 2, "app_url": "https://x", "entity_type": "epic-comment",
+                "text": "reply", "author_id": "b", "deleted": false,
+                "mention_ids": [], "member_mention_ids": [],
+                "group_mention_ids": [], "external_id": null,
+                "comments": [],
+                "created_at": "2026-01-01T00:00:00Z",
+                "updated_at": "2026-01-01T00:00:00Z"
+            }],
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let c = ThreadedComment::from_json(&v).unwrap();
+        assert_eq!(c.text, "outer");
+        assert_eq!(c.comments.len(), 1);
+        assert_eq!(c.comments[0].text, "reply");
+    }
+
+    #[test]
+    fn create_epic_params_minimal() {
+        let p = CreateEpicParams::default()
+            .name("Q2 platform work")
+            .group_id("uuid-platform");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(
+            s,
+            r#"{"name":"Q2 platform work","group_id":"uuid-platform"}"#
+        );
+    }
+
+    #[test]
+    fn search_query_builds_path() {
+        let q = SearchQuery::new("owner:ijanc state:in-progress")
+            .page_size(25)
+            .detail("slim");
+        let p = q.append_to("/api/v3/search/stories");
+        assert_eq!(
+            p,
+            "/api/v3/search/stories?\
+             query=owner%3Aijanc%20state%3Ain-progress\
+             &page_size=25\
+             &detail=slim"
+        );
+    }
+
+    #[test]
+    fn search_query_with_entity_types_joins_comma() {
+        let q = SearchQuery::new("x").entity_types(vec!["story".to_string(), "epic".to_string()]);
+        let p = q.append_to("/api/v3/search");
+        assert!(p.contains("entity_types=story%2Cepic"));
+    }
+
+    #[test]
+    fn iteration_round_trip_with_stats() {
+        let raw = r##"{
+            "id": 7, "global_id": "v3:iter:7", "entity_type": "iteration",
+            "app_url": "https://app.shortcut.com/x/iteration/7",
+            "name": "Sprint 12", "description": "",
+            "status": "started",
+            "start_date": "2026-04-01", "end_date": "2026-04-14",
+            "group_ids": ["g1"], "follower_ids": [], "label_ids": [],
+            "labels": [], "mention_ids": [], "member_mention_ids": [],
+            "group_mention_ids": [], "associated_groups": [],
+            "stats": {
+                "num_points": 50, "num_points_done": 30,
+                "num_points_started": 10, "num_points_unstarted": 5,
+                "num_points_backlog": 5,
+                "num_stories_done": 6, "num_stories_started": 2,
+                "num_stories_unstarted": 1, "num_stories_backlog": 1,
+                "num_stories_unestimated": 0, "num_related_documents": 0,
+                "average_cycle_time": 86400, "average_lead_time": 172800
+            },
+            "created_at": "2026-03-25T00:00:00Z",
+            "updated_at": "2026-04-01T00:00:00Z"
+        }"##;
+        let v: Value = raw.parse().unwrap();
+        let it = Iteration::from_json(&v).unwrap();
+        assert_eq!(it.name, "Sprint 12");
+        assert_eq!(it.stats.num_points_done, 30);
+        assert_eq!(it.stats.average_cycle_time, Some(86400));
+    }
+
+    #[test]
+    fn create_iteration_params_emits_required() {
+        let p = CreateIterationParams::default()
+            .name("Sprint 13")
+            .start_date("2026-04-15")
+            .end_date("2026-04-28");
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(
+            s,
+            r#"{"name":"Sprint 13","start_date":"2026-04-15","end_date":"2026-04-28"}"#
+        );
+    }
+
+    #[test]
+    fn create_story_params_skips_none() {
+        let p = CreateStoryParams::default()
+            .name("write SDK")
+            .workflow_state_id(500)
+            .owner_ids(vec!["uuid-1".to_string()]);
+        let s = p.to_json().stringify().unwrap();
+        assert_eq!(
+            s,
+            r#"{"name":"write SDK","workflow_state_id":500,"owner_ids":["uuid-1"]}"#
+        );
+    }
+
+    #[test]
+    fn workflow_round_trip_with_states() {
+        let raw = r#"{
+            "id": 500,
+            "entity_type": "workflow",
+            "name": "Default",
+            "description": "",
+            "team_id": 1,
+            "default_state_id": 500001,
+            "project_ids": [],
+            "auto_assign_owner": true,
+            "states": [{
+                "id": 500001, "global_id": "v3:state:500001",
+                "entity_type": "workflow-state", "name": "Backlog",
+                "description": "", "color": null, "verb": null, "type": "unstarted",
+                "position": 100, "num_stories": 0, "num_story_templates": 0,
+                "created_at": "2026-01-01T00:00:00Z",
+                "updated_at": "2026-01-01T00:00:00Z"
+            }],
+            "created_at": "2026-01-01T00:00:00Z",
+            "updated_at": "2026-01-01T00:00:00Z"
+        }"#;
+        let v: Value = raw.parse().unwrap();
+        let w = Workflow::from_json(&v).unwrap();
+        assert_eq!(w.id, 500);
+        assert_eq!(w.states.len(), 1);
+        assert_eq!(w.states[0].kind, "unstarted");
+    }
+}
blob - /dev/null
blob + 801088a610ad72ae57a656211710d353ec6821e1 (mode 644)
--- /dev/null
+++ shortcut_swagger.json
@@ -0,0 +1,17129 @@
+{
+  "schemes": [
+    "https"
+  ],
+  "securityDefinitions": {
+    "api_token": {
+      "type": "apiKey",
+      "in": "header",
+      "name": "Shortcut-Token"
+    }
+  },
+  "definitions": {
+    "BaseTaskParams": {
+      "description": "Request parameters for specifying how to pre-populate a task through a template.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 2048,
+          "minLength": 1,
+          "description": "The Task description.",
+          "type": "string"
+        },
+        "complete": {
+          "description": "True/false boolean indicating whether the Task is completed. Defaults to false.",
+          "type": "boolean"
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the Task has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description"
+      ]
+    },
+    "BasicWorkspaceInfo": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "type": "string",
+          "format": "date-time"
+        },
+        "default_workflow_id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "estimate_scale": {
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "type": "string"
+        },
+        "url_slug": {
+          "type": "string"
+        },
+        "utc_offset": {
+          "type": "string"
+        },
+        "korey_enabled": {
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "created_at",
+        "default_workflow_id",
+        "estimate_scale",
+        "name",
+        "url_slug",
+        "utc_offset"
+      ]
+    },
+    "Branch": {
+      "description": "Branch refers to a VCS branch. Branches are feature branches associated with Shortcut Stories.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "deleted": {
+          "description": "A true/false boolean indicating if the Branch has been deleted.",
+          "type": "boolean"
+        },
+        "name": {
+          "description": "The name of the Branch.",
+          "type": "string"
+        },
+        "persistent": {
+          "description": "This field is deprecated, and will always be false.",
+          "type": "boolean"
+        },
+        "updated_at": {
+          "description": "The time/date the Branch was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "pull_requests": {
+          "description": "An array of PullRequests attached to the Branch (there is usually only one).",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/PullRequest"
+          }
+        },
+        "merged_branch_ids": {
+          "description": "The IDs of the Branches the Branch has been merged into.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "id": {
+          "description": "The unique ID of the Branch.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "url": {
+          "description": "The URL of the Branch.",
+          "type": "string"
+        },
+        "repository_id": {
+          "description": "The ID of the Repository that contains the Branch.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Branch was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "deleted",
+        "name",
+        "persistent",
+        "updated_at",
+        "pull_requests",
+        "merged_branch_ids",
+        "id",
+        "url",
+        "repository_id",
+        "created_at"
+      ]
+    },
+    "Category": {
+      "description": "A Category can be used to associate Objectives.",
+      "type": "object",
+      "properties": {
+        "archived": {
+          "description": "A true/false boolean indicating if the Category has been archived.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "color": {
+          "description": "The hex color to be displayed with the Category (for example, \"#ff0000\").",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the Category.",
+          "type": "string"
+        },
+        "global_id": {
+          "description": "The Global ID of the Category.",
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "type": {
+          "description": "The type of entity this Category is associated with; currently Milestone or Objective is the only type of Category.",
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date that the Category was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Category has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Category.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date that the Category was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "archived",
+        "entity_type",
+        "color",
+        "name",
+        "global_id",
+        "type",
+        "updated_at",
+        "external_id",
+        "id",
+        "created_at"
+      ]
+    },
+    "Commit": {
+      "description": "Commit refers to a VCS commit and all associated details.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The ID of the Member that authored the Commit, if known.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "hash": {
+          "description": "The Commit hash.",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date the Commit was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Commit.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "url": {
+          "description": "The URL of the Commit.",
+          "type": "string"
+        },
+        "author_email": {
+          "description": "The email address of the VCS user that authored the Commit.",
+          "type": "string"
+        },
+        "timestamp": {
+          "description": "The time/date the Commit was pushed.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "author_identity": {
+          "$ref": "#/definitions/Identity"
+        },
+        "repository_id": {
+          "description": "The ID of the Repository that contains the Commit.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "created_at": {
+          "description": "The time/date the Commit was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "message": {
+          "description": "The Commit message.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "author_id",
+        "hash",
+        "updated_at",
+        "id",
+        "url",
+        "author_email",
+        "timestamp",
+        "author_identity",
+        "repository_id",
+        "created_at",
+        "message"
+      ]
+    },
+    "ConflictError": {
+      "type": "object",
+      "properties": {
+        "message": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "message"
+      ]
+    },
+    "CreateCategory": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The name of the new Category.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The hex color to be displayed with the Category (for example, \"#ff0000\").",
+          "minLength": 1,
+          "type": "string"
+        },
+        "external_id": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "This field can be set to another unique ID. In the case that the Category has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "type": {
+          "description": "The type of entity this Category is associated with; currently Milestone or Objective is the only type of Category.",
+          "x-doc-skip": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateCategoryParams": {
+      "description": "Request parameters for creating a Category with a Objective.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The name of the new Category.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The hex color to be displayed with the Category (for example, \"#ff0000\").",
+          "minLength": 1,
+          "type": "string"
+        },
+        "external_id": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "This field can be set to another unique ID. In the case that the Category has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateCommentComment": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "text": {
+          "maxLength": 100000,
+          "description": "The comment text.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The Member ID of the Comment's author. Defaults to the user identified by the API token.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date the comment is created, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date the comment is last updated, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the comment has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "text"
+      ]
+    },
+    "CreateDoc": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "title": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The title for the new document.",
+          "type": "string"
+        },
+        "content": {
+          "description": "The content for the new document.",
+          "type": "string"
+        },
+        "content_format": {
+          "description": "Format of the content being sent. Defaults to 'html'. If 'markdown', content will be converted to HTML for storage. Storage is always HTML; this parameter just tells us what format you're sending.",
+          "type": "string",
+          "enum": [
+            "markdown",
+            "html"
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "title",
+        "content"
+      ]
+    },
+    "CreateEntityTemplate": {
+      "description": "Request parameters for creating an entirely new entity template.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The name of the new entity template",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The id of the user creating this template.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "story_contents": {
+          "$ref": "#/definitions/CreateStoryContents"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "story_contents"
+      ]
+    },
+    "CreateEpic": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The Epic's description.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Epic.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "objective_ids": {
+          "description": "An array of IDs for Objectives to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The Epic's name.",
+          "type": "string"
+        },
+        "planned_start_date": {
+          "description": "The Epic's planned start date.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "state": {
+          "description": "`Deprecated` The Epic's state (to do, in progress, or done); will be ignored when `epic_state_id` is set.",
+          "type": "string",
+          "enum": [
+            "in progress",
+            "to do",
+            "done"
+          ]
+        },
+        "milestone_id": {
+          "description": "`Deprecated` The ID of the Milestone this Epic is related to. Use `objective_ids`.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the member that requested the epic.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "epic_state_id": {
+          "description": "The ID of the Epic State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_id": {
+          "description": "`Deprecated` The ID of the group to associate with the epic. Use `group_ids`.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date it is created but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers on this new Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDS for Groups to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "converted_from_story_id": {
+          "description": "The ID of the Story that was converted to an Epic.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the Epic has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "deadline": {
+          "description": "The Epic's deadline.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "created_at": {
+          "description": "Defaults to the time/date it is created but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateEpicComment": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "text": {
+          "maxLength": 100000,
+          "description": "The comment text.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The Member ID of the Comment's author. Defaults to the user identified by the API token.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date the comment is created, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date the comment is last updated, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the comment has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "text"
+      ]
+    },
+    "CreateEpicHealth": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "status": {
+          "description": "The health status of the Epic.",
+          "type": "string",
+          "enum": [
+            "At Risk",
+            "On Track",
+            "Off Track",
+            "No Health"
+          ]
+        },
+        "text": {
+          "description": "The description of the Health status.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "status"
+      ]
+    },
+    "CreateGenericIntegration": {
+      "type": "object",
+      "properties": {
+        "webhook_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "type": "string"
+        },
+        "secret": {
+          "maxLength": 128,
+          "minLength": 1,
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "webhook_url"
+      ]
+    },
+    "CreateGroup": {
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 4096,
+          "description": "The description of the Group.",
+          "type": "string"
+        },
+        "member_ids": {
+          "description": "The Member ids to add to this Group.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "workflow_ids": {
+          "description": "The Workflow ids to add to the Group.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "The name of this Group.",
+          "type": "string"
+        },
+        "mention_name": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "The mention name of this Group.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The color you wish to use for the Group in the system.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "color_key": {
+          "description": "The color key you wish to use for the Group in the system.",
+          "type": "string",
+          "enum": [
+            "blue",
+            "purple",
+            "midnight-blue",
+            "orange",
+            "yellow-green",
+            "brass",
+            "gray",
+            "fuchsia",
+            "yellow",
+            "pink",
+            "sky-blue",
+            "green",
+            "red",
+            "black",
+            "slate",
+            "turquoise"
+          ]
+        },
+        "display_icon_id": {
+          "description": "The Icon id for the avatar of this Group.",
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "mention_name"
+      ]
+    },
+    "CreateIteration": {
+      "type": "object",
+      "properties": {
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDs for any Groups you want to add as Followers. Currently, only one Group association is presented in our web UI.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Iteration.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "description": {
+          "maxLength": 100000,
+          "description": "The description of the Iteration.",
+          "type": "string"
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of this Iteration.",
+          "type": "string"
+        },
+        "start_date": {
+          "minLength": 1,
+          "description": "The date this Iteration begins, e.g. 2019-07-01.",
+          "type": "string"
+        },
+        "end_date": {
+          "minLength": 1,
+          "description": "The date this Iteration ends, e.g. 2019-07-01.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "start_date",
+        "end_date"
+      ]
+    },
+    "CreateLabelParams": {
+      "description": "Request parameters for creating a Label on a Shortcut Story.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The name of the new Label.",
+          "type": "string"
+        },
+        "description": {
+          "maxLength": 1024,
+          "description": "The description of the new Label.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The hex color to be displayed with the Label (for example, \"#ff0000\").",
+          "minLength": 1,
+          "type": "string"
+        },
+        "external_id": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "This field can be set to another unique ID. In the case that the Label has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateLinkedFile": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 512,
+          "description": "The description of the file.",
+          "type": "string"
+        },
+        "story_id": {
+          "description": "The ID of the linked story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of the file.",
+          "type": "string"
+        },
+        "thumbnail_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The URL of the thumbnail, if the integration provided it.",
+          "type": "string"
+        },
+        "type": {
+          "description": "The integration type of the file (e.g. google, dropbox, box).",
+          "type": "string",
+          "enum": [
+            "google",
+            "url",
+            "dropbox",
+            "box",
+            "onedrive"
+          ]
+        },
+        "size": {
+          "description": "The filesize, if the integration provided it.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "uploader_id": {
+          "description": "The UUID of the member that uploaded the file.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "content_type": {
+          "maxLength": 128,
+          "description": "The content type of the image (e.g. txt/plain).",
+          "type": "string"
+        },
+        "url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The URL of linked file.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "type",
+        "url"
+      ]
+    },
+    "CreateMilestone": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of the Milestone.",
+          "type": "string"
+        },
+        "description": {
+          "maxLength": 100000,
+          "description": "The Milestone's description.",
+          "type": "string"
+        },
+        "state": {
+          "description": "The workflow state that the Milestone is in.",
+          "type": "string",
+          "enum": [
+            "in progress",
+            "to do",
+            "done"
+          ]
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Milestone was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Milestone was completed.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "categories": {
+          "description": "An array of IDs of Categories attached to the Milestone.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateCategoryParams"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateObjective": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of the Objective.",
+          "type": "string"
+        },
+        "description": {
+          "maxLength": 100000,
+          "description": "The Objective's description.",
+          "type": "string"
+        },
+        "state": {
+          "description": "The workflow state that the Objective is in.",
+          "type": "string",
+          "enum": [
+            "in progress",
+            "to do",
+            "done"
+          ]
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Objective was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Objective was completed.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "categories": {
+          "description": "An array of IDs of Categories attached to the Objective.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateCategoryParams"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateObjectiveHealth": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "status": {
+          "description": "The health status of the Objective.",
+          "type": "string",
+          "enum": [
+            "At Risk",
+            "On Track",
+            "Off Track",
+            "No Health"
+          ]
+        },
+        "text": {
+          "description": "The description of the Health status.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "status"
+      ]
+    },
+    "CreateOrDeleteStoryReaction": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "emoji": {
+          "description": "The emoji short-code to add / remove. E.g. `:thumbsup::skin-tone-4:`.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "emoji"
+      ]
+    },
+    "CreateProject": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The Project description.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The color you wish to use for the Project in the system.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The name of the Project.",
+          "type": "string"
+        },
+        "start_time": {
+          "description": "The date at which the Project was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date it is created but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the Project has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "team_id": {
+          "description": "The ID of the team the project belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "iteration_length": {
+          "description": "The number of weeks per iteration in this Project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "abbreviation": {
+          "maxLength": 63,
+          "description": "The Project abbreviation used in Story summaries. Should be kept to 3 characters at most.",
+          "type": "string"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date it is created but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name",
+        "team_id"
+      ]
+    },
+    "CreateStories": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "stories": {
+          "description": "An array of stories to be created.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateStoryParams"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "stories"
+      ]
+    },
+    "CreateStoryComment": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "text": {
+          "maxLength": 100000,
+          "description": "The comment text.",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The Member ID of the Comment's author. Defaults to the user identified by the API token.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date the comment is created, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date the comment is last updated, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "external_id": {
+          "maxLength": 1024,
+          "description": "This field can be set to another unique ID. In the case that the comment has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "parent_id": {
+          "description": "The ID of the Comment that this comment is threaded under.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "text"
+      ]
+    },
+    "CreateStoryCommentParams": {
+      "description": "Request parameters for creating a Comment on a Shortcut Story.",
+      "type": "object",
+      "properties": {
+        "text": {
+          "maxLength": 100000,
+          "description": "The comment text.",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The Member ID of the Comment's author. Defaults to the user identified by the API token.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date the comment is created, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date the comment is last updated, but can be set to reflect another date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "external_id": {
+          "maxLength": 1024,
+          "description": "This field can be set to another unique ID. In the case that the comment has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "parent_id": {
+          "description": "The ID of the Comment that this comment is threaded under.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "text"
+      ]
+    },
+    "CreateStoryContents": {
+      "description": "A map of story attributes this template populates.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels to be populated by the template.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string"
+        },
+        "custom_fields": {
+          "description": "An array of maps specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "file_ids": {
+          "description": "An array of the attached file IDs to be populated.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "name": {
+          "maxLength": 512,
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "epic_id": {
+          "description": "The ID of the epic the to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of external links to be populated.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "sub_tasks": {
+          "description": "An array of maps specifying the sub-tasks to create and link to the story.\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateSubTaskParams"
+          }
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "tasks": {
+          "description": "An array of tasks to be populated by the template.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/BaseTaskParams"
+          }
+        },
+        "group_id": {
+          "description": "The ID of the group to be populated.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "estimate": {
+          "description": "The numeric point estimate to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "linked_file_ids": {
+          "description": "An array of the linked file IDs to be populated.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false
+    },
+    "CreateStoryFromTemplateParams": {
+      "description": "Request parameters for creating a story from a story template. These parameters are merged with the values derived from the template.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "Controls the story's archived state.",
+          "type": "boolean"
+        },
+        "story_links": {
+          "description": "An array of story links attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateStoryLinkParams"
+          }
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "external_links_add": {
+          "description": "An array of External Links associated with this story. These will be added to any links provided by the template. Cannot be used in conjunction with `external_links`.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "uniqueItems": true
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "custom_fields": {
+          "description": "A map specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "move_to": {
+          "description": "One of \"first\" or \"last\". This can be used to move the given story to the first or last position in the workflow state.",
+          "type": "string",
+          "enum": [
+            "last",
+            "first"
+          ]
+        },
+        "file_ids": {
+          "description": "An array of IDs of files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "source_task_id": {
+          "description": "Given this story was converted from a task in another story, this is the original task ID that was converted to this story.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "name": {
+          "maxLength": 512,
+          "minLength": 1,
+          "description": "The name of the story. Must be provided if the template does not provide a name.",
+          "type": "string"
+        },
+        "file_ids_add": {
+          "description": "An array of IDs of files attached to the story in addition to files from the template. Cannot be used in conjunction with `file_ids`.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "file_ids_remove": {
+          "description": "An array of IDs of files removed from files from the template. Cannot be used in conjunction with `file_ids`.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "comments": {
+          "description": "An array of comments to add to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateStoryCommentParams"
+          }
+        },
+        "follower_ids_add": {
+          "description": "The UUIDs of the new followers to be added in addition to followers from the template. Cannot be used in conjunction with `follower_ids.`",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "story_template_id": {
+          "description": "The id of the story template used to create this story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "external_links": {
+          "description": "An array of External Links associated with this story.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "follower_ids_remove": {
+          "description": "The UUIDs of the new followers to be removed from followers from the template. Cannot be used in conjunction with `follower_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "sub_tasks": {
+          "description": "An array of maps specifying sub-tasks to be associated with the created story. Each map can either link to an existing story or create a new sub-task story to be linked to the created story.\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "x-oneOf": [
+              {
+                "$ref": "#/definitions/LinkSubTaskParams"
+              },
+              {
+                "$ref": "#/definitions/CreateSubTaskParams"
+              }
+            ]
+          }
+        },
+        "linked_file_ids_remove": {
+          "description": "An array of IDs of linked files removed from files from the template. Cannot be used in conjunction with `linked_files.`",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "custom_fields_remove": {
+          "description": "A map specifying a CustomField ID. These will be removed from any fields provided by the template. Cannot be used in conjunction with `custom_fields`.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/RemoveCustomFieldParams"
+          },
+          "uniqueItems": true
+        },
+        "tasks": {
+          "description": "An array of tasks connected to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateTaskParams"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Story was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "labels_add": {
+          "description": "An array of labels attached to the story in addition to the labels provided by the template. Cannot be used in conjunction with `labels`.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          },
+          "uniqueItems": true
+        },
+        "group_id": {
+          "description": "The id of the group to associate with this story.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story will be in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The time/date the Story was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs of the followers of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "external_id": {
+          "maxLength": 1024,
+          "description": "This field can be set to another unique ID. In the case that the Story has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "parent_story_id": {
+          "description": "The ID of the parent story to associate with this story (making the created story a sub-task).\nField only applicable when Sub-task feature is enabled.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "owner_ids_remove": {
+          "description": "The UUIDs of the new owners to be removed from owners from the template. Cannot be used in conjunction with `owners`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "custom_fields_add": {
+          "description": "A map specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField. These will be added to any fields provided by the template. Cannot be used in conjunction with `custom_fields`.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          },
+          "uniqueItems": true
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "linked_file_ids_add": {
+          "description": "An array of IDs of linked files attached to the story in addition to files from the template. Cannot be used in conjunction with `linked_files`.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "linked_file_ids": {
+          "description": "An array of IDs of linked files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "labels_remove": {
+          "description": "An array of labels to remove from the labels provided by the template. Cannot be used in conjunction with `labels`.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/RemoveLabelParams"
+          },
+          "uniqueItems": true
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "owner_ids_add": {
+          "description": "The UUIDs of the new owners to be added in addition to owners from the template. Cannot be used in conjunction with `owners`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "created_at": {
+          "description": "The time/date the Story was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "external_links_remove": {
+          "description": "An array of External Links associated with this story. These will be removed from any links provided by the template. Cannot be used in conjunction with `external_links`.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "uniqueItems": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "story_template_id"
+      ]
+    },
+    "CreateStoryLink": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "verb": {
+          "description": "The type of link.",
+          "type": "string",
+          "enum": [
+            "blocks",
+            "duplicates",
+            "relates to"
+          ]
+        },
+        "subject_id": {
+          "description": "The ID of the subject Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "object_id": {
+          "description": "The ID of the object Story.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "verb",
+        "subject_id",
+        "object_id"
+      ]
+    },
+    "CreateStoryLinkParams": {
+      "description": "Request parameters for creating a Story Link within a Story.",
+      "type": "object",
+      "properties": {
+        "subject_id": {
+          "description": "The unique ID of the Story defined as subject.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "verb": {
+          "description": "How the subject Story acts on the object Story. This can be \"blocks\", \"duplicates\", or \"relates to\".",
+          "type": "string",
+          "enum": [
+            "blocks",
+            "duplicates",
+            "relates to"
+          ]
+        },
+        "object_id": {
+          "description": "The unique ID of the Story defined as object.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "verb"
+      ]
+    },
+    "CreateStoryParams": {
+      "description": "Request parameters for creating a story.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "Controls the story's archived state.",
+          "type": "boolean"
+        },
+        "story_links": {
+          "description": "An array of story links attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateStoryLinkParams"
+          }
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "custom_fields": {
+          "description": "A map specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "move_to": {
+          "description": "One of \"first\" or \"last\". This can be used to move the given story to the first or last position in the workflow state.",
+          "type": "string",
+          "enum": [
+            "last",
+            "first"
+          ]
+        },
+        "file_ids": {
+          "description": "An array of IDs of files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "source_task_id": {
+          "description": "Given this story was converted from a task in another story, this is the original task ID that was converted to this story.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "name": {
+          "maxLength": 512,
+          "minLength": 1,
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "comments": {
+          "description": "An array of comments to add to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateStoryCommentParams"
+          }
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "story_template_id": {
+          "description": "The id of the story template used to create this story, if applicable. This is just an association; no content from the story template is inherited by the story simply by setting this field.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of External Links associated with this story.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "sub_tasks": {
+          "description": "An array of maps specifying sub-tasks to be associated with the created story. Each map can either link to an existing story or create a new sub-task story to be linked to the created story.\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "x-oneOf": [
+              {
+                "$ref": "#/definitions/LinkSubTaskParams"
+              },
+              {
+                "$ref": "#/definitions/CreateSubTaskParams"
+              }
+            ]
+          }
+        },
+        "requested_by_id": {
+          "description": "The ID of the member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "tasks": {
+          "description": "An array of tasks connected to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateTaskParams"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Story was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_id": {
+          "description": "The id of the group to associate with this story.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story will be in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The time/date the Story was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs of the followers of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "external_id": {
+          "maxLength": 1024,
+          "description": "This field can be set to another unique ID. In the case that the Story has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "parent_story_id": {
+          "description": "The ID of the parent story to associate with this story (making the created story a sub-task).\nField only applicable when Sub-task feature is enabled.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "linked_file_ids": {
+          "description": "An array of IDs of linked files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "created_at": {
+          "description": "The time/date the Story was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateSubTaskParams": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 512,
+          "minLength": 1,
+          "description": "The name of the SubTask.",
+          "type": "string"
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story will be in.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "CreateTask": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 2048,
+          "minLength": 1,
+          "description": "The Task description.",
+          "type": "string"
+        },
+        "complete": {
+          "description": "True/false boolean indicating whether the Task is completed. Defaults to false.",
+          "type": "boolean"
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the Task has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date the Task is created but can be set to reflect another creation time/date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date the Task is created in Shortcut but can be set to reflect another time/date.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description"
+      ]
+    },
+    "CreateTaskParams": {
+      "description": "Request parameters for creating a Task on a Story.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 2048,
+          "minLength": 1,
+          "description": "The Task description.",
+          "type": "string"
+        },
+        "complete": {
+          "description": "True/false boolean indicating whether the Task is completed. Defaults to false.",
+          "type": "boolean"
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the Task has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "created_at": {
+          "description": "Defaults to the time/date the Task is created but can be set to reflect another creation time/date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "Defaults to the time/date the Task is created in Shortcut but can be set to reflect another time/date.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description"
+      ]
+    },
+    "CustomField": {
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 512,
+          "minLength": 1,
+          "description": "A string description of the CustomField",
+          "type": "string"
+        },
+        "icon_set_identifier": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "A string that represents the icon that corresponds to this custom field.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string",
+          "enum": [
+            "custom-field"
+          ]
+        },
+        "story_types": {
+          "description": "The types of stories this CustomField is scoped to.",
+          "x-doc-skip": true,
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "name": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "The name of the Custom Field.",
+          "type": "string"
+        },
+        "fixed_position": {
+          "description": "When true, the CustomFieldEnumValues may not be reordered.",
+          "x-doc-skip": true,
+          "type": "boolean"
+        },
+        "updated_at": {
+          "description": "The instant when this CustomField was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "id": {
+          "description": "The unique public ID for the CustomField.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "values": {
+          "description": "A collection of legal values for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldEnumValue"
+          }
+        },
+        "field_type": {
+          "description": "The type of Custom Field, eg. 'enum'.",
+          "type": "string",
+          "enum": [
+            "enum"
+          ]
+        },
+        "position": {
+          "description": "An integer indicating the position of this Custom Field with respect to the other CustomField",
+          "type": "integer",
+          "format": "int64"
+        },
+        "canonical_name": {
+          "description": "The canonical name for a Shortcut-defined field.",
+          "type": "string"
+        },
+        "enabled": {
+          "description": "When true, the CustomField can be applied to entities in the Workspace.",
+          "type": "boolean"
+        },
+        "created_at": {
+          "description": "The instant when this CustomField was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "name",
+        "updated_at",
+        "id",
+        "field_type",
+        "position",
+        "enabled",
+        "created_at"
+      ]
+    },
+    "CustomFieldEnumValue": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The unique public ID for the Custom Field.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "value": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "A string value within the domain of this Custom Field.",
+          "type": "string"
+        },
+        "position": {
+          "description": "An integer indicating the position of this Value with respect to the other CustomFieldEnumValues in the enumeration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "color_key": {
+          "description": "A color key associated with this CustomFieldEnumValue.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string",
+          "enum": [
+            "custom-field-enum-value"
+          ]
+        },
+        "enabled": {
+          "description": "When true, the CustomFieldEnumValue can be selected for the CustomField.",
+          "x-doc-skip": true,
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "value",
+        "position",
+        "color_key",
+        "entity_type",
+        "enabled"
+      ]
+    },
+    "CustomFieldValueParams": {
+      "type": "object",
+      "properties": {
+        "field_id": {
+          "description": "The unique public ID for the CustomField.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "value_id": {
+          "description": "The unique public ID for the CustomFieldEnumValue.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "value": {
+          "description": "A literal value for the CustomField. Currently ignored.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "field_id",
+        "value_id"
+      ]
+    },
+    "DataConflictError": {
+      "description": "Error returned when Datomic tx fails due to Datomc :db.error/cas-failed error",
+      "type": "object",
+      "properties": {
+        "error": {
+          "type": "string",
+          "enum": [
+            "data-conflict-error"
+          ]
+        },
+        "message": {
+          "description": "An explanatory message: \"The update failed due to a data conflict. Please refresh and try again.\"",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "error",
+        "message"
+      ]
+    },
+    "DeleteStories": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "story_ids": {
+          "description": "An array of IDs of Stories to delete.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "story_ids"
+      ]
+    },
+    "DisabledFeatureError": {
+      "type": "object",
+      "properties": {
+        "feature_tag": {
+          "description": "The feature that is disabled",
+          "type": "string"
+        },
+        "message": {
+          "description": "The message explaining the error",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "feature_tag",
+        "message"
+      ]
+    },
+    "Doc": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The public id of the Doc",
+          "type": "string",
+          "format": "uuid"
+        },
+        "title": {
+          "description": "The Doc's title",
+          "type": "string",
+          "x-nullable": true
+        },
+        "content_markdown": {
+          "description": "The Doc's content in Markdown format (converted from HTML storage).",
+          "type": "string",
+          "x-nullable": true
+        },
+        "content_html": {
+          "description": "The Doc's content in HTML format (as stored in S3). Only included when include_html=true query parameter is provided.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "app_url": {
+          "description": "The Shortcut application url for the Doc",
+          "type": "string"
+        },
+        "created_at": {
+          "description": "The time/date the Doc was created",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The time/date the Doc was last updated",
+          "type": "string",
+          "format": "date-time"
+        },
+        "archived": {
+          "description": "Whether the Doc is archived",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "title",
+        "content_markdown",
+        "app_url",
+        "created_at",
+        "updated_at",
+        "archived"
+      ]
+    },
+    "DocSearchResults": {
+      "description": "The results of the Document search query.",
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "total": {
+          "description": "The total number of matches for the search query. The first 1000 matches can be paged through via the API.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "data": {
+          "description": "A list of document search results.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/DocSlim"
+          }
+        },
+        "next": {
+          "description": "The URL path and query string for the next page of search results.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "total",
+        "data",
+        "next"
+      ]
+    },
+    "DocSlim": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The public id of the Doc",
+          "type": "string",
+          "format": "uuid"
+        },
+        "title": {
+          "description": "The Docs Title",
+          "type": "string",
+          "x-nullable": true
+        },
+        "app_url": {
+          "description": "The Shortcut application url for the Doc.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "title",
+        "app_url"
+      ]
+    },
+    "EntityTemplate": {
+      "description": "An entity template can be used to prefill various fields when creating new stories.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "id": {
+          "description": "The unique identifier for the entity template.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "description": "The time/date when the entity template was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The time/date when the entity template was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "name": {
+          "description": "The template's name.",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The unique ID of the member who created the template.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "last_used_at": {
+          "description": "The last time that someone created an entity using this template.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "story_contents": {
+          "$ref": "#/definitions/StoryContents"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "id",
+        "created_at",
+        "updated_at",
+        "name",
+        "author_id",
+        "last_used_at",
+        "story_contents"
+      ]
+    },
+    "Epic": {
+      "description": "An Epic is a collection of stories that together might make up a release, a objective, or some other large initiative that you are working on.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Epic.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The Epic's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True/false boolean that indicates whether the Epic is archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Epic has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Epic.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Epic description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "associated_groups": {
+          "description": "An array containing Group IDs and Group-owned story counts for the Epic's associated groups.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/EpicAssociatedGroup"
+          }
+        },
+        "project_ids": {
+          "description": "The IDs of Projects related to this Epic.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "stories_without_projects": {
+          "description": "The number of stories in this epic which are not associated with a project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "productboard_plugin_id": {
+          "description": "The ID of the associated productboard integration.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "objective_ids": {
+          "description": "An array of IDs for Objectives to which this epic is related.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "description": "The name of the Epic.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Epic has been completed.",
+          "type": "boolean"
+        },
+        "comments": {
+          "description": "A nested array of threaded comments.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ThreadedComment"
+          }
+        },
+        "productboard_url": {
+          "description": "The URL of the associated productboard feature.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "planned_start_date": {
+          "description": "The Epic's planned start date.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "state": {
+          "description": "`Deprecated` The workflow state that the Epic is in.",
+          "type": "string"
+        },
+        "milestone_id": {
+          "description": "`Deprecated` The ID of the Objective this Epic is related to. Use `objective_ids`.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the epic.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "epic_state_id": {
+          "description": "The ID of the Epic State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "label_ids": {
+          "description": "An array of Label ids attached to the Epic.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "`Deprecated` The ID of the group to associate with the epic. Use `group_ids`.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the Epic was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Epic description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "productboard_id": {
+          "description": "The ID of the associated productboard feature.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers on this Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDS for Groups to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Epic has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "health": {
+          "$ref": "#/definitions/Health"
+        },
+        "position": {
+          "description": "The Epic's relative position in the Epic workflow state.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "productboard_name": {
+          "description": "The name of the associated productboard feature.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "deadline": {
+          "description": "The Epic's deadline.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/EpicStats"
+        },
+        "created_at": {
+          "description": "The time/date the Epic was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "started",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "associated_groups",
+        "project_ids",
+        "stories_without_projects",
+        "completed_at_override",
+        "productboard_plugin_id",
+        "started_at",
+        "completed_at",
+        "objective_ids",
+        "name",
+        "global_id",
+        "completed",
+        "comments",
+        "productboard_url",
+        "planned_start_date",
+        "state",
+        "milestone_id",
+        "requested_by_id",
+        "epic_state_id",
+        "label_ids",
+        "started_at_override",
+        "group_id",
+        "updated_at",
+        "group_mention_ids",
+        "productboard_id",
+        "follower_ids",
+        "group_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "position",
+        "productboard_name",
+        "deadline",
+        "stats",
+        "created_at"
+      ]
+    },
+    "EpicAssociatedGroup": {
+      "type": "object",
+      "properties": {
+        "group_id": {
+          "description": "The Group ID of the associated group.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "associated_stories_count": {
+          "description": "The number of stories this Group owns in the Epic.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "group_id"
+      ]
+    },
+    "EpicPaginatedResults": {
+      "description": "Results schema for paginated Epic listing.",
+      "type": "object",
+      "properties": {
+        "data": {
+          "description": "Array of Epic objects on the current page",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/EpicSlim"
+          }
+        },
+        "next": {
+          "description": "The next page number if there are more results, or null for the last page",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "total": {
+          "description": "The total number of Epics matching the query over all pages",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "data",
+        "next",
+        "total"
+      ]
+    },
+    "EpicSearchResult": {
+      "description": "An Epic in search results. This is typed differently from Epic because the details=slim search argument will omit some fields.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Epic.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The Epic's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True/false boolean that indicates whether the Epic is archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Epic has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Epic.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Epic description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "associated_groups": {
+          "description": "An array containing Group IDs and Group-owned story counts for the Epic's associated groups.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/EpicAssociatedGroup"
+          }
+        },
+        "project_ids": {
+          "description": "The IDs of Projects related to this Epic.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "stories_without_projects": {
+          "description": "The number of stories in this epic which are not associated with a project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "productboard_plugin_id": {
+          "description": "The ID of the associated productboard integration.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "objective_ids": {
+          "description": "An array of IDs for Objectives to which this epic is related.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "description": "The name of the Epic.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Epic has been completed.",
+          "type": "boolean"
+        },
+        "comments": {
+          "description": "A nested array of threaded comments.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ThreadedComment"
+          }
+        },
+        "productboard_url": {
+          "description": "The URL of the associated productboard feature.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "planned_start_date": {
+          "description": "The Epic's planned start date.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "state": {
+          "description": "`Deprecated` The workflow state that the Epic is in.",
+          "type": "string"
+        },
+        "milestone_id": {
+          "description": "`Deprecated` The ID of the Objective this Epic is related to. Use `objective_ids`.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the epic.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "epic_state_id": {
+          "description": "The ID of the Epic State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "label_ids": {
+          "description": "An array of Label ids attached to the Epic.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "`Deprecated` The ID of the group to associate with the epic. Use `group_ids`.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the Epic was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Epic description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "productboard_id": {
+          "description": "The ID of the associated productboard feature.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers on this Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDS for Groups to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Epic has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "health": {
+          "$ref": "#/definitions/Health"
+        },
+        "position": {
+          "description": "The Epic's relative position in the Epic workflow state.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "productboard_name": {
+          "description": "The name of the associated productboard feature.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "deadline": {
+          "description": "The Epic's deadline.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/EpicStats"
+        },
+        "created_at": {
+          "description": "The time/date the Epic was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "archived",
+        "started",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "associated_groups",
+        "project_ids",
+        "stories_without_projects",
+        "completed_at_override",
+        "productboard_plugin_id",
+        "started_at",
+        "completed_at",
+        "objective_ids",
+        "name",
+        "global_id",
+        "completed",
+        "productboard_url",
+        "planned_start_date",
+        "state",
+        "milestone_id",
+        "requested_by_id",
+        "epic_state_id",
+        "label_ids",
+        "started_at_override",
+        "group_id",
+        "updated_at",
+        "group_mention_ids",
+        "productboard_id",
+        "follower_ids",
+        "group_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "position",
+        "productboard_name",
+        "deadline",
+        "stats",
+        "created_at"
+      ]
+    },
+    "EpicSearchResults": {
+      "description": "The results of the Epic search query.",
+      "type": "object",
+      "properties": {
+        "total": {
+          "description": "The total number of matches for the search query. The first 1000 matches can be paged through via the API.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "data": {
+          "description": "A list of search results.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/EpicSearchResult"
+          }
+        },
+        "next": {
+          "description": "The URL path and query string for the next page of search results.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "total",
+        "data",
+        "next"
+      ]
+    },
+    "EpicSlim": {
+      "description": "EpicSlim represents the same resource as an Epic but is more light-weight, including all Epic fields except the comments array. The description string can be optionally included. Use the [Get Epic](#Get-Epic) endpoint to fetch the unabridged payload for an Epic.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Epic.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The Epic's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True/false boolean that indicates whether the Epic is archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Epic has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Epic.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Epic description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "associated_groups": {
+          "description": "An array containing Group IDs and Group-owned story counts for the Epic's associated groups.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/EpicAssociatedGroup"
+          }
+        },
+        "project_ids": {
+          "description": "The IDs of Projects related to this Epic.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "stories_without_projects": {
+          "description": "The number of stories in this epic which are not associated with a project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "productboard_plugin_id": {
+          "description": "The ID of the associated productboard integration.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "objective_ids": {
+          "description": "An array of IDs for Objectives to which this epic is related.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "description": "The name of the Epic.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Epic has been completed.",
+          "type": "boolean"
+        },
+        "productboard_url": {
+          "description": "The URL of the associated productboard feature.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "planned_start_date": {
+          "description": "The Epic's planned start date.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "state": {
+          "description": "`Deprecated` The workflow state that the Epic is in.",
+          "type": "string"
+        },
+        "milestone_id": {
+          "description": "`Deprecated` The ID of the Objective this Epic is related to. Use `objective_ids`.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the epic.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "epic_state_id": {
+          "description": "The ID of the Epic State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "label_ids": {
+          "description": "An array of Label ids attached to the Epic.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "`Deprecated` The ID of the group to associate with the epic. Use `group_ids`.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the Epic was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Epic description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "productboard_id": {
+          "description": "The ID of the associated productboard feature.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers on this Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDS for Groups to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this new Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Epic has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "position": {
+          "description": "The Epic's relative position in the Epic workflow state.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "productboard_name": {
+          "description": "The name of the associated productboard feature.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "deadline": {
+          "description": "The Epic's deadline.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/EpicStats"
+        },
+        "created_at": {
+          "description": "The time/date the Epic was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "archived",
+        "started",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "associated_groups",
+        "project_ids",
+        "stories_without_projects",
+        "completed_at_override",
+        "productboard_plugin_id",
+        "started_at",
+        "completed_at",
+        "objective_ids",
+        "name",
+        "global_id",
+        "completed",
+        "productboard_url",
+        "planned_start_date",
+        "state",
+        "milestone_id",
+        "requested_by_id",
+        "epic_state_id",
+        "label_ids",
+        "started_at_override",
+        "group_id",
+        "updated_at",
+        "group_mention_ids",
+        "productboard_id",
+        "follower_ids",
+        "group_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "position",
+        "productboard_name",
+        "deadline",
+        "stats",
+        "created_at"
+      ]
+    },
+    "EpicState": {
+      "description": "Epic State is any of the at least 3 columns. Epic States correspond to one of 3 types: Unstarted, Started, or Done.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of what sort of Epics belong in that Epic State.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The hex color for this Epic State.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "name": {
+          "description": "The Epic State's name.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "type": {
+          "description": "The type of Epic State (Unstarted, Started, or Done)",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "When the Epic State was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "id": {
+          "description": "The unique ID of the Epic State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "position": {
+          "description": "The position that the Epic State is in, starting with 0 at the left.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Epic State was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "name",
+        "global_id",
+        "type",
+        "updated_at",
+        "id",
+        "position",
+        "created_at"
+      ]
+    },
+    "EpicStats": {
+      "description": "A group of calculated values for this Epic.",
+      "type": "object",
+      "properties": {
+        "num_points_done": {
+          "description": "The total number of completed points in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_related_documents": {
+          "description": "The total number of documents associated with this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_unstarted": {
+          "description": "The total number of unstarted Stories in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_total": {
+          "description": "The total number of Stories in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "last_story_update": {
+          "description": "The date of the last update of a Story in this Epic.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "num_points_started": {
+          "description": "The total number of started points in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_unstarted": {
+          "description": "The total number of unstarted points in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_started": {
+          "description": "The total number of started Stories in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_unestimated": {
+          "description": "The total number of Stories with no point estimate.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_backlog": {
+          "description": "The total number of backlog Stories in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_backlog": {
+          "description": "The total number of backlog points in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points": {
+          "description": "The total number of points in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_done": {
+          "description": "The total number of done Stories in this Epic.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_points_done",
+        "num_related_documents",
+        "num_stories_unstarted",
+        "num_stories_total",
+        "last_story_update",
+        "num_points_started",
+        "num_points_unstarted",
+        "num_stories_started",
+        "num_stories_unestimated",
+        "num_stories_backlog",
+        "num_points_backlog",
+        "num_points",
+        "num_stories_done"
+      ]
+    },
+    "EpicWorkflow": {
+      "description": "Epic Workflow is the array of defined Epic States. Epic Workflow can be queried using the API but must be updated in the Shortcut UI. ",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "id": {
+          "description": "The unique ID of the Epic Workflow.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The date the Epic Workflow was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The date the Epic Workflow was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "default_epic_state_id": {
+          "description": "The unique ID of the default Epic State that new Epics are assigned by default.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "epic_states": {
+          "description": "A map of the Epic States in this Epic Workflow.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/EpicState"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "id",
+        "created_at",
+        "updated_at",
+        "default_epic_state_id",
+        "epic_states"
+      ]
+    },
+    "GetDoc": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "content_format": {
+          "description": "Format of the content to return. Defaults to 'markdown'. If 'html', includes HTML content in response.",
+          "type": "string",
+          "enum": [
+            "markdown",
+            "html"
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "Group": {
+      "description": "A Group.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Group.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Group.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "Whether or not the Group is archived.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "color": {
+          "description": "The hex color to be displayed with the Group (for example, \"#ff0000\").",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "num_stories_started": {
+          "description": "The number of stories assigned to the group which are in a started workflow state.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "mention_name": {
+          "pattern": "^[a-z0-9\\-\\_\\.]+$",
+          "minLength": 1,
+          "description": "The mention name of the Group.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the Group.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "color_key": {
+          "description": "The color key to be displayed with the Group.",
+          "type": "string",
+          "enum": [
+            "blue",
+            "purple",
+            "midnight-blue",
+            "orange",
+            "yellow-green",
+            "brass",
+            "gray",
+            "fuchsia",
+            "yellow",
+            "pink",
+            "sky-blue",
+            "green",
+            "red",
+            "black",
+            "slate",
+            "turquoise"
+          ],
+          "x-nullable": true
+        },
+        "num_stories": {
+          "description": "The total number of stories assigned to the group.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_epics_started": {
+          "description": "The number of epics assigned to the group which are in the started workflow state.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The last instant when this group was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "num_stories_backlog": {
+          "description": "The number of stories assigned to the group which are in a backlog workflow state.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "id": {
+          "description": "The id of the Group.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "display_icon": {
+          "$ref": "#/definitions/Icon",
+          "x-nullable": true
+        },
+        "default_workflow_id": {
+          "description": "The ID of the default workflow for stories created in this group.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "member_ids": {
+          "description": "The Member IDs contain within the Group.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "workflow_ids": {
+          "description": "The Workflow IDs contained within the Group.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "created_at": {
+          "description": "The instant when this group was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "entity_type",
+        "color",
+        "num_stories_started",
+        "mention_name",
+        "name",
+        "global_id",
+        "color_key",
+        "num_stories",
+        "num_epics_started",
+        "updated_at",
+        "num_stories_backlog",
+        "id",
+        "display_icon",
+        "member_ids",
+        "workflow_ids",
+        "created_at"
+      ]
+    },
+    "Health": {
+      "description": "The current health status of the Epic.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The ID of the permission who created or updated the Health record.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "epic_id": {
+          "description": "The ID of the Epic associated with this Health record.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "objective_id": {
+          "description": "The ID of the Objective associated with this Health record.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The time that the Health record was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "status": {
+          "description": "The health status of the Epic or Objective.",
+          "type": "string",
+          "enum": [
+            "At Risk",
+            "On Track",
+            "Off Track",
+            "No Health"
+          ]
+        },
+        "id": {
+          "description": "The unique ID of the Health record.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "created_at": {
+          "description": "The time that the Health record was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "text": {
+          "description": "The text of the Health record.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "status",
+        "id"
+      ]
+    },
+    "History": {
+      "description": "A history item is a group of actions that represent a transactional change to a Story.",
+      "type": "object",
+      "properties": {
+        "actor_name": {
+          "description": "The name of the actor that performed the action, if it can be determined.",
+          "type": "string"
+        },
+        "changed_at": {
+          "description": "The date when the change occurred.",
+          "type": "string"
+        },
+        "primary_id": {
+          "description": "The ID of the primary entity that has changed, if applicable.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "references": {
+          "description": "An array of objects affected by the change. Reference objects provide basic information for the entities reference in the history actions. Some have specific fields, but they always contain an id, entity_type, and a name.",
+          "type": "array",
+          "items": {
+            "x-oneOf": [
+              {
+                "$ref": "#/definitions/HistoryReferenceBranch"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceCommit"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceEpic"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceGroup"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceIteration"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceLabel"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceProject"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceStory"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceStoryTask"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceCustomFieldEnumValue"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceWorkflowState"
+              },
+              {
+                "$ref": "#/definitions/HistoryReferenceGeneral"
+              }
+            ]
+          }
+        },
+        "actions": {
+          "description": "An array of actions that were performed for the change.",
+          "type": "array",
+          "items": {
+            "x-oneOf": [
+              {
+                "$ref": "#/definitions/HistoryActionBranchCreate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionBranchMerge"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionBranchPush"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionLabelCreate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionLabelUpdate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionLabelDelete"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionProjectUpdate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionPullRequest"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryCreate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryUpdate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryDelete"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryCommentCreate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryLinkCreate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryLinkUpdate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionStoryLinkDelete"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionTaskCreate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionTaskUpdate"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionTaskDelete"
+              },
+              {
+                "$ref": "#/definitions/HistoryActionWorkspace2BulkUpdate"
+              }
+            ]
+          }
+        },
+        "member_id": {
+          "description": "The ID of the member who performed the change.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "external_id": {
+          "description": "The ID of the webhook that handled the change.",
+          "type": "string"
+        },
+        "id": {
+          "description": "The ID representing the change for the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "version": {
+          "description": "The version of the change format.",
+          "type": "string",
+          "enum": [
+            "v1"
+          ]
+        },
+        "webhook_id": {
+          "description": "The ID of the webhook that handled the change.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "automation_id": {
+          "description": "The ID of the automation that performed the change.",
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "changed_at",
+        "actions",
+        "id",
+        "version"
+      ]
+    },
+    "HistoryActionBranchCreate": {
+      "description": "An action representing a VCS Branch being created.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the VCS Branch that was pushed",
+          "type": "string"
+        },
+        "url": {
+          "description": "The URL from the provider of the VCS Branch that was pushed",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "create"
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "name",
+        "url",
+        "action"
+      ]
+    },
+    "HistoryActionBranchMerge": {
+      "description": "An action representing a VCS Branch being merged.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the VCS Branch that was pushed",
+          "type": "string"
+        },
+        "url": {
+          "description": "The URL from the provider of the VCS Branch that was pushed",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "merge"
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "name",
+        "url",
+        "action"
+      ]
+    },
+    "HistoryActionBranchPush": {
+      "description": "An action representing a VCS Branch being pushed.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the VCS Branch that was pushed",
+          "type": "string"
+        },
+        "url": {
+          "description": "The URL from the provider of the VCS Branch that was pushed",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "push"
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "name",
+        "url",
+        "action"
+      ]
+    },
+    "HistoryActionLabelCreate": {
+      "description": "An action representing a Label being created.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "create"
+          ]
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Label.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the Label.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "app_url",
+        "name"
+      ]
+    },
+    "HistoryActionLabelDelete": {
+      "description": "An action representing a Label being deleted.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "delete"
+          ]
+        },
+        "name": {
+          "description": "The name of the Label.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "name"
+      ]
+    },
+    "HistoryActionLabelUpdate": {
+      "description": "An action representing a Label being updated.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "update"
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action"
+      ]
+    },
+    "HistoryActionProjectUpdate": {
+      "description": "An action representing a Project being updated.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "update"
+          ]
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Project.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the Project.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "app_url",
+        "name"
+      ]
+    },
+    "HistoryActionPullRequest": {
+      "description": "An action representing various operations for a Pull Request.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "open",
+            "update",
+            "reopen",
+            "close",
+            "sync",
+            "comment"
+          ]
+        },
+        "number": {
+          "description": "The VCS Repository-specific ID for the Pull Request.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "title": {
+          "description": "The title of the Pull Request.",
+          "type": "string"
+        },
+        "url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The URL from the provider of the VCS Pull Request.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "number",
+        "title",
+        "url"
+      ]
+    },
+    "HistoryActionStoryCommentCreate": {
+      "description": "An action representing a Story Comment being created.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "create"
+          ]
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Story Comment.",
+          "type": "string"
+        },
+        "author_id": {
+          "description": "The Member ID of who created the Story Comment.",
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "app_url",
+        "author_id"
+      ]
+    },
+    "HistoryActionStoryCreate": {
+      "description": "An action representing a Story being created.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Story.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Story.",
+          "type": "string"
+        },
+        "started": {
+          "description": "Whether or not the Story has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "task_ids": {
+          "description": "An array of Task IDs on this Story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "story_type": {
+          "description": "The type of Story; either feature, bug, or chore.",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "name": {
+          "description": "The name of the Story.",
+          "type": "string"
+        },
+        "completed": {
+          "description": "Whether or not the Story is completed.",
+          "type": "boolean"
+        },
+        "blocker": {
+          "description": "Whether or not the Story is blocking another Story.",
+          "type": "boolean"
+        },
+        "epic_id": {
+          "description": "The Epic ID for this Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the Story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The Iteration ID the Story is in.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "label_ids": {
+          "description": "An array of Labels IDs attached to the Story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "group_id": {
+          "description": "The Team IDs for the followers of the Story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "workflow_state_id": {
+          "description": "An array of Workflow State IDs attached to the Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "object_story_link_ids": {
+          "description": "An array of Story IDs that are the object of a Story Link relationship.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "follower_ids": {
+          "description": "An array of Member IDs for the followers of the Story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of Member IDs that are the owners of the Story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "custom_field_value_ids": {
+          "description": "An array of Custom Field Enum Value ids on this Story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "parent_story_id": {
+          "description": "The Story's Parent ID (only applicable if Story is a Sub-task)",
+          "type": "integer",
+          "format": "int64"
+        },
+        "estimate": {
+          "description": "The estimate (or point value) for the Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "subject_story_link_ids": {
+          "description": "An array of Story IDs that are the subject of a Story Link relationship.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "create"
+          ]
+        },
+        "blocked": {
+          "description": "Whether or not the Story is blocked by another Story.",
+          "type": "boolean"
+        },
+        "project_id": {
+          "description": "The Project ID of the Story is in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "deadline": {
+          "description": "The timestamp representing the Story's deadline.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "entity_type",
+        "story_type",
+        "name",
+        "id",
+        "action"
+      ]
+    },
+    "HistoryActionStoryDelete": {
+      "description": "An action representing a Story being deleted.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "delete"
+          ]
+        },
+        "name": {
+          "description": "The name of the Story.",
+          "type": "string"
+        },
+        "story_type": {
+          "description": "The type of Story; either feature, bug, or chore.",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "parent_story_id": {
+          "description": "The Story's Parent ID (only applicable if Story is a Sub-task)",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "name",
+        "story_type"
+      ]
+    },
+    "HistoryActionStoryLinkCreate": {
+      "description": "An action representing a Story Link being created.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "create"
+          ]
+        },
+        "verb": {
+          "description": "The verb describing the link's relationship.",
+          "type": "string",
+          "enum": [
+            "blocks",
+            "duplicates",
+            "relates to"
+          ]
+        },
+        "subject_id": {
+          "description": "The Story ID of the subject Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "object_id": {
+          "description": "The Story ID of the object Story.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "verb",
+        "subject_id",
+        "object_id"
+      ]
+    },
+    "HistoryActionStoryLinkDelete": {
+      "description": "An action representing a Story Link being deleted.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "delete"
+          ]
+        },
+        "verb": {
+          "description": "The verb describing the link's relationship.",
+          "type": "string",
+          "enum": [
+            "blocks",
+            "duplicates",
+            "relates to"
+          ]
+        },
+        "subject_id": {
+          "description": "The Story ID of the subject Story.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "object_id": {
+          "description": "The Story ID of the object Story.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "verb",
+        "subject_id",
+        "object_id"
+      ]
+    },
+    "HistoryActionStoryLinkUpdate": {
+      "description": "An action representing a Story Link being updated.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "update"
+          ]
+        },
+        "verb": {
+          "description": "The verb describing the link's relationship.",
+          "type": "string",
+          "enum": [
+            "blocks",
+            "duplicates",
+            "relates to"
+          ]
+        },
+        "subject_id": {
+          "description": "The Story ID of the subject Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "object_id": {
+          "description": "The Story ID of the object Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "changes": {
+          "$ref": "#/definitions/HistoryChangesStoryLink"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "verb",
+        "subject_id",
+        "object_id",
+        "changes"
+      ]
+    },
+    "HistoryActionStoryUpdate": {
+      "description": "An action representing a Story being updated.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "update"
+          ]
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Story.",
+          "type": "string"
+        },
+        "changes": {
+          "$ref": "#/definitions/HistoryChangesStory"
+        },
+        "name": {
+          "description": "The name of the Story.",
+          "type": "string"
+        },
+        "story_type": {
+          "description": "The type of Story; either feature, bug, or chore.",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "parent_story_id": {
+          "description": "The Story's Parent ID (only applicable if Story is a Sub-task)",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "app_url",
+        "name",
+        "story_type"
+      ]
+    },
+    "HistoryActionTaskCreate": {
+      "description": "An action representing a Task being created.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of the Task.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "mention_ids": {
+          "description": "An array of Member IDs that represent who has been mentioned in the Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_mention_ids": {
+          "description": "An array of Groups IDs that represent which have been mentioned in the Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of Member IDs that represent the Task's owners.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "create"
+          ]
+        },
+        "complete": {
+          "description": "Whether or not the Task is complete.",
+          "type": "boolean"
+        },
+        "deadline": {
+          "description": "A timestamp that represent's the Task's deadline.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "id",
+        "action",
+        "complete"
+      ]
+    },
+    "HistoryActionTaskDelete": {
+      "description": "An action representing a Task being deleted.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "delete"
+          ]
+        },
+        "description": {
+          "description": "The description of the Task being deleted.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "description"
+      ]
+    },
+    "HistoryActionTaskUpdate": {
+      "description": "An action representing a Task being updated.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "update"
+          ]
+        },
+        "changes": {
+          "$ref": "#/definitions/HistoryChangesTask"
+        },
+        "complete": {
+          "description": "Whether or not the Task is complete.",
+          "type": "boolean"
+        },
+        "description": {
+          "description": "The description of the Task.",
+          "type": "string"
+        },
+        "story_id": {
+          "description": "The Story ID that contains the Task.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "changes",
+        "description",
+        "story_id"
+      ]
+    },
+    "HistoryActionWorkspace2BulkUpdate": {
+      "description": "An action representing a bulk operation within a workspace2.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "action": {
+          "description": "The action of the entity referenced.",
+          "type": "string",
+          "enum": [
+            "bulk-update"
+          ]
+        },
+        "name": {
+          "description": "The name of the workspace2 in which the BulkUpdate occurred.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "action",
+        "name"
+      ]
+    },
+    "HistoryChangesStory": {
+      "description": "The changes that have occurred as a result of the action.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewStr"
+        },
+        "archived": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewBool"
+        },
+        "started": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewBool"
+        },
+        "task_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesInt"
+        },
+        "mention_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesUuid"
+        },
+        "story_type": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewStr"
+        },
+        "name": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewStr"
+        },
+        "completed": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewBool"
+        },
+        "blocker": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewBool"
+        },
+        "epic_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "branch_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesInt"
+        },
+        "commit_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesInt"
+        },
+        "requested_by_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewUuid"
+        },
+        "iteration_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "label_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesInt"
+        },
+        "group_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewUuid"
+        },
+        "workflow_state_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "object_story_link_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesInt"
+        },
+        "follower_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesUuid"
+        },
+        "owner_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesUuid"
+        },
+        "custom_field_value_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesUuid"
+        },
+        "parent_story_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "estimate": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "subject_story_link_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesInt"
+        },
+        "blocked": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewBool"
+        },
+        "project_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "deadline": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewStr"
+        }
+      },
+      "additionalProperties": false
+    },
+    "HistoryChangesStoryLink": {
+      "description": "The changes that have occurred as a result of the action.",
+      "type": "object",
+      "properties": {
+        "verb": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewStr"
+        },
+        "object_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        },
+        "subject_id": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewInt"
+        }
+      },
+      "additionalProperties": false
+    },
+    "HistoryChangesTask": {
+      "description": "The changes that have occurred as a result of the action.",
+      "type": "object",
+      "properties": {
+        "complete": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewBool"
+        },
+        "description": {
+          "$ref": "#/definitions/StoryHistoryChangeOldNewStr"
+        },
+        "mention_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesUuid"
+        },
+        "owner_ids": {
+          "$ref": "#/definitions/StoryHistoryChangeAddsRemovesUuid"
+        }
+      },
+      "additionalProperties": false
+    },
+    "HistoryReferenceBranch": {
+      "description": "A reference to a VCS Branch.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        },
+        "url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The external URL for the Branch.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "name",
+        "url"
+      ]
+    },
+    "HistoryReferenceCommit": {
+      "description": "A reference to a VCS Commit.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "message": {
+          "description": "The message from the Commit.",
+          "type": "string"
+        },
+        "url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The external URL for the Branch.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "message",
+        "url"
+      ]
+    },
+    "HistoryReferenceCustomFieldEnumValue": {
+      "description": "A reference to a CustomField value asserted on a Story.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "string_value": {
+          "description": "The custom-field enum value as a string.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "enum_value_enabled": {
+          "description": "Whether or not the custom-field enum value is enabled.",
+          "type": "boolean",
+          "x-nullable": true
+        },
+        "field_id": {
+          "description": "The public-id of the parent custom-field of this enum value.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "field_type": {
+          "description": "The type variety of the parent custom-field of this enum value.",
+          "type": "string"
+        },
+        "field_name": {
+          "description": "The name as it is displayed to the user of the parent custom-field of this enum value.",
+          "type": "string"
+        },
+        "field_enabled": {
+          "description": "Whether or not the custom-field is enabled.",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "string_value",
+        "enum_value_enabled",
+        "field_id",
+        "field_type",
+        "field_name",
+        "field_enabled"
+      ]
+    },
+    "HistoryReferenceEpic": {
+      "description": "A reference to an Epic.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Epic.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "app_url",
+        "name"
+      ]
+    },
+    "HistoryReferenceGeneral": {
+      "description": "A default reference for entity types that don't have extra fields.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "name"
+      ]
+    },
+    "HistoryReferenceGroup": {
+      "description": "A reference to a Group.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "name"
+      ]
+    },
+    "HistoryReferenceIteration": {
+      "description": "A reference to an Iteration.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Iteration.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "app_url",
+        "name"
+      ]
+    },
+    "HistoryReferenceLabel": {
+      "description": "A reference to an Label.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Label.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "app_url",
+        "name"
+      ]
+    },
+    "HistoryReferenceProject": {
+      "description": "A reference to an Project.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Project.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "app_url",
+        "name"
+      ]
+    },
+    "HistoryReferenceStory": {
+      "description": "A reference to a Story.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "app_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The application URL of the Story.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        },
+        "story_type": {
+          "description": "If the referenced entity is a Story, either \"bug\", \"chore\", or \"feature\".",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "parent_story_id": {
+          "description": "The Story's Parent ID (only applicable if Story is a Sub-task)",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "app_url",
+        "name",
+        "story_type"
+      ]
+    },
+    "HistoryReferenceStoryTask": {
+      "description": "A reference to a Story Task.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Story Task.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "description"
+      ]
+    },
+    "HistoryReferenceWorkflowState": {
+      "description": "A references to a Story Workflow State.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the entity referenced.",
+          "x-oneOf": [
+            {
+              "type": "integer",
+              "format": "int64"
+            },
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        "entity_type": {
+          "description": "The type of entity referenced.",
+          "type": "string"
+        },
+        "type": {
+          "description": "Either \"backlog\", \"unstarted\", \"started\", or \"done\".",
+          "type": "string",
+          "enum": [
+            "started",
+            "backlog",
+            "unstarted",
+            "done"
+          ]
+        },
+        "name": {
+          "description": "The name of the entity referenced.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "entity_type",
+        "type",
+        "name"
+      ]
+    },
+    "Icon": {
+      "description": "Icons are used to attach images to Groups, Workspaces, Members, and Loading screens in the Shortcut web application.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "id": {
+          "description": "The unique ID of the Icon.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "created_at": {
+          "description": "The time/date that the Icon was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The time/date that the Icon was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "url": {
+          "description": "The URL of the Icon.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "id",
+        "created_at",
+        "updated_at",
+        "url"
+      ]
+    },
+    "Identity": {
+      "description": "The Identity of the VCS user that authored the Commit.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "name": {
+          "description": "This is your login in VCS.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "type": {
+          "description": "The service this Identity is for.",
+          "type": "string",
+          "enum": [
+            "slack",
+            "github",
+            "gitlab",
+            "bitbucket"
+          ],
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "name",
+        "type"
+      ]
+    },
+    "Iteration": {
+      "description": "An Iteration is a defined, time-boxed period of development for a collection of Stories. See https://help.shortcut.com/hc/en-us/articles/360028953452-Iterations-Overview for more information.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Iteration.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the iteration.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels attached to the iteration.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Label"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "associated_groups": {
+          "description": "An array containing Group IDs and Group-owned story counts for the Iteration's associated groups.",
+          "x-doc-skip": true,
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/IterationAssociatedGroup"
+          }
+        },
+        "name": {
+          "description": "The name of the iteration.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "label_ids": {
+          "description": "An array of label ids attached to the iteration.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "updated_at": {
+          "description": "The instant when this iteration was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "end_date": {
+          "description": "The date this iteration ends.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDs for any Groups you want to add as Followers. Currently, only one Group association is presented in our web UI.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "start_date": {
+          "description": "The date this iteration begins.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "status": {
+          "description": "The status of the iteration. Values are either \"unstarted\", \"started\", or \"done\".",
+          "type": "string"
+        },
+        "id": {
+          "description": "The ID of the iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "stats": {
+          "$ref": "#/definitions/IterationStats"
+        },
+        "created_at": {
+          "description": "The instant when this iteration was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "associated_groups",
+        "name",
+        "global_id",
+        "label_ids",
+        "updated_at",
+        "group_mention_ids",
+        "end_date",
+        "follower_ids",
+        "group_ids",
+        "start_date",
+        "status",
+        "id",
+        "stats",
+        "created_at"
+      ]
+    },
+    "IterationAssociatedGroup": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "group_id": {
+          "description": "The Group ID of the associated group.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "associated_stories_count": {
+          "description": "The number of stories this Group owns in the Iteration.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "group_id"
+      ]
+    },
+    "IterationSearchResults": {
+      "description": "The results of the Iteration search query.",
+      "type": "object",
+      "properties": {
+        "total": {
+          "description": "The total number of matches for the search query. The first 1000 matches can be paged through via the API.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "data": {
+          "description": "A list of search results.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/IterationSlim"
+          }
+        },
+        "next": {
+          "description": "The URL path and query string for the next page of search results.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "total",
+        "data",
+        "next"
+      ]
+    },
+    "IterationSlim": {
+      "description": "IterationSlim represents the same resource as an Iteration, but is more light-weight. Use the [Get Iteration](#Get-Iteration) endpoint to fetch the unabridged payload for an Iteration. ",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Iteration.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels attached to the iteration.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Label"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "associated_groups": {
+          "description": "An array containing Group IDs and Group-owned story counts for the Iteration's associated groups.",
+          "x-doc-skip": true,
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/IterationAssociatedGroup"
+          }
+        },
+        "name": {
+          "description": "The name of the iteration.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "label_ids": {
+          "description": "An array of label ids attached to the iteration.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "updated_at": {
+          "description": "The instant when this iteration was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "end_date": {
+          "description": "The date this iteration ends.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDs for any Groups you want to add as Followers. Currently, only one Group association is presented in our web UI.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "start_date": {
+          "description": "The date this iteration begins.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "status": {
+          "description": "The status of the iteration. Values are either \"unstarted\", \"started\", or \"done\".",
+          "type": "string"
+        },
+        "id": {
+          "description": "The ID of the iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "stats": {
+          "$ref": "#/definitions/IterationStats"
+        },
+        "created_at": {
+          "description": "The instant when this iteration was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "associated_groups",
+        "name",
+        "global_id",
+        "label_ids",
+        "updated_at",
+        "group_mention_ids",
+        "end_date",
+        "follower_ids",
+        "group_ids",
+        "start_date",
+        "status",
+        "id",
+        "stats",
+        "created_at"
+      ]
+    },
+    "IterationStats": {
+      "description": "A group of calculated values for this Iteration.",
+      "type": "object",
+      "properties": {
+        "num_points_done": {
+          "description": "The total number of completed points in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_related_documents": {
+          "description": "The total number of documents related to an Iteration",
+          "type": "integer",
+          "format": "int64"
+        },
+        "average_cycle_time": {
+          "description": "The average cycle time (in seconds) of completed stories in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_unstarted": {
+          "description": "The total number of unstarted Stories in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_started": {
+          "description": "The total number of started points in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_unstarted": {
+          "description": "The total number of unstarted points in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_started": {
+          "description": "The total number of started Stories in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_unestimated": {
+          "description": "The total number of Stories with no point estimate.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_backlog": {
+          "description": "The total number of backlog Stories in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "average_lead_time": {
+          "description": "The average lead time (in seconds) of completed stories in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_backlog": {
+          "description": "The total number of backlog points in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points": {
+          "description": "The total number of points in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_done": {
+          "description": "The total number of done Stories in this Iteration.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_points_done",
+        "num_related_documents",
+        "num_stories_unstarted",
+        "num_points_started",
+        "num_points_unstarted",
+        "num_stories_started",
+        "num_stories_unestimated",
+        "num_stories_backlog",
+        "num_points_backlog",
+        "num_points",
+        "num_stories_done"
+      ]
+    },
+    "KeyResult": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The ID of the Key Result.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "name": {
+          "description": "The name of the Key Result.",
+          "type": "string"
+        },
+        "objective_id": {
+          "description": "The Objective to which this Key Result belongs.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "type": {
+          "description": "The type of the Key Result (numeric, percent, or boolean).",
+          "type": "string",
+          "enum": [
+            "percent",
+            "boolean",
+            "numeric"
+          ]
+        },
+        "initial_observed_value": {
+          "$ref": "#/definitions/KeyResultValue"
+        },
+        "current_observed_value": {
+          "$ref": "#/definitions/KeyResultValue"
+        },
+        "current_target_value": {
+          "$ref": "#/definitions/KeyResultValue"
+        },
+        "progress": {
+          "description": "The integer percentage of progress toward completion of the Key Result.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "name",
+        "objective_id",
+        "type",
+        "initial_observed_value",
+        "current_observed_value",
+        "current_target_value",
+        "progress"
+      ]
+    },
+    "KeyResultValue": {
+      "description": "The starting value of the Key Result.",
+      "type": "object",
+      "properties": {
+        "numeric_value": {
+          "description": "The numeric value, as a decimal string. No more than two decimal places are allowed.",
+          "type": "string"
+        },
+        "boolean_value": {
+          "description": "The boolean value.",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "Label": {
+      "description": "A Label can be used to associate and filter Stories and Epics, and also create new Workspaces.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Label.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Label.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "archived": {
+          "description": "A true/false boolean indicating if the Label has been archived.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "color": {
+          "description": "The hex color to be displayed with the Label (for example, \"#ff0000\").",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the Label.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date that the Label was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Label has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "stats": {
+          "$ref": "#/definitions/LabelStats"
+        },
+        "created_at": {
+          "description": "The time/date that the Label was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "entity_type",
+        "color",
+        "name",
+        "global_id",
+        "updated_at",
+        "external_id",
+        "id",
+        "created_at"
+      ]
+    },
+    "LabelSlim": {
+      "description": "A Label can be used to associate and filter Stories and Epics, and also create new Workspaces. A slim Label does not include aggregate stats. Fetch the Label using the labels endpoint to retrieve them.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Label.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Label.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "archived": {
+          "description": "A true/false boolean indicating if the Label has been archived.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "color": {
+          "description": "The hex color to be displayed with the Label (for example, \"#ff0000\").",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the Label.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date that the Label was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Label has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date that the Label was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "entity_type",
+        "color",
+        "name",
+        "global_id",
+        "updated_at",
+        "external_id",
+        "id",
+        "created_at"
+      ]
+    },
+    "LabelStats": {
+      "description": "A group of calculated values for this Label. This is not included if the slim? flag is set to true for the List Labels endpoint.",
+      "type": "object",
+      "properties": {
+        "num_related_documents": {
+          "description": "The total number of Documents associated this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_epics": {
+          "description": "The total number of Epics with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_unstarted": {
+          "description": "The total number of stories unstarted Stories with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_total": {
+          "description": "The total number of Stories with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_epics_unstarted": {
+          "description": "The number of unstarted epics associated with this label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_epics_in_progress": {
+          "description": "The number of in progress epics associated with this label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_unstarted": {
+          "description": "The total number of unstarted points with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_unestimated": {
+          "description": "The total number of Stories with no point estimate with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_in_progress": {
+          "description": "The total number of in-progress points with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_epics_total": {
+          "description": "The total number of Epics associated with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_completed": {
+          "description": "The total number of completed Stories with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_completed": {
+          "description": "The total number of completed points with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_backlog": {
+          "description": "The total number of stories backlog Stories with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_total": {
+          "description": "The total number of points with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_stories_in_progress": {
+          "description": "The total number of in-progress Stories with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points_backlog": {
+          "description": "The total number of backlog points with this Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_epics_completed": {
+          "description": "The number of completed Epics associated with this Label.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_related_documents",
+        "num_epics",
+        "num_stories_unstarted",
+        "num_stories_total",
+        "num_epics_unstarted",
+        "num_epics_in_progress",
+        "num_points_unstarted",
+        "num_stories_unestimated",
+        "num_points_in_progress",
+        "num_epics_total",
+        "num_stories_completed",
+        "num_points_completed",
+        "num_stories_backlog",
+        "num_points_total",
+        "num_stories_in_progress",
+        "num_points_backlog",
+        "num_epics_completed"
+      ]
+    },
+    "LinkSubTaskParams": {
+      "type": "object",
+      "properties": {
+        "story_id": {
+          "description": "The ID of the story to link as a sub-task of the parent story",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "story_id"
+      ]
+    },
+    "LinkedFile": {
+      "description": "Linked files are stored on a third-party website and linked to one or more Stories. Shortcut currently supports linking files from Google Drive, Dropbox, Box, and by URL.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of the file.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "story_ids": {
+          "description": "The IDs of the stories this file is attached to.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "The members that are mentioned in the description of the file.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "name": {
+          "description": "The name of the linked file.",
+          "type": "string"
+        },
+        "thumbnail_url": {
+          "description": "The URL of the file thumbnail, if the integration provided it.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "type": {
+          "description": "The integration type (e.g. google, dropbox, box).",
+          "type": "string"
+        },
+        "size": {
+          "description": "The filesize, if the integration provided it.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "uploader_id": {
+          "description": "The UUID of the member that uploaded the file.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "content_type": {
+          "description": "The content type of the image (e.g. txt/plain).",
+          "type": "string",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the LinkedFile was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_mention_ids": {
+          "description": "The groups that are mentioned in the description of the file.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "id": {
+          "description": "The unique identifier for the file.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "url": {
+          "description": "The URL of the file.",
+          "type": "string"
+        },
+        "created_at": {
+          "description": "The time/date the LinkedFile was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "story_ids",
+        "mention_ids",
+        "member_mention_ids",
+        "name",
+        "thumbnail_url",
+        "type",
+        "size",
+        "uploader_id",
+        "content_type",
+        "updated_at",
+        "group_mention_ids",
+        "id",
+        "url",
+        "created_at"
+      ]
+    },
+    "MaxSearchResultsExceededError": {
+      "description": "Error returned when total maximum supported results have been reached.",
+      "type": "object",
+      "properties": {
+        "error": {
+          "description": "The name for this type of error, `maximum-results-exceeded`",
+          "type": "string",
+          "enum": [
+            "maximum-results-exceeded"
+          ]
+        },
+        "message": {
+          "description": "An explanatory message: \"A maximum of 1000 search results are supported.\"",
+          "type": "string"
+        },
+        "maximum-results": {
+          "description": "The maximum number of search results supported, `1000`",
+          "type": "integer",
+          "format": "int64",
+          "enum": [
+            1000
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "error",
+        "message",
+        "maximum-results"
+      ]
+    },
+    "Member": {
+      "description": "Details about an individual user within the Workspace.",
+      "type": "object",
+      "properties": {
+        "role": {
+          "description": "The Member's role in the Workspace.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "disabled": {
+          "description": "True/false boolean indicating whether the Member has been disabled within the Workspace.",
+          "type": "boolean"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "state": {
+          "description": "The user state, one of partial, full, disabled, or imported.  A partial user is disabled, has no means to log in, and is not an import user.  A full user is enabled and has a means to log in.  A disabled user is disabled and has a means to log in.  An import user is disabled, has no means to log in, and is marked as an import user.",
+          "type": "string",
+          "enum": [
+            "partial",
+            "full",
+            "disabled",
+            "imported"
+          ]
+        },
+        "updated_at": {
+          "description": "The time/date the Member was last updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "created_without_invite": {
+          "description": "Whether this member was created as a placeholder entity.",
+          "x-doc-skip": true,
+          "type": "boolean"
+        },
+        "group_ids": {
+          "description": "The Member's group ids",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "id": {
+          "description": "The Member's ID in Shortcut.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "installation_id": {
+          "description": "Only set for agents. The installation id associated with this agent.",
+          "x-doc-skip": true,
+          "type": "string",
+          "format": "uuid"
+        },
+        "profile": {
+          "$ref": "#/definitions/Profile"
+        },
+        "created_at": {
+          "description": "The time/date the Member was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "replaced_by": {
+          "description": "The id of the member that replaces this one when merged.",
+          "x-doc-skip": true,
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "role",
+        "entity_type",
+        "disabled",
+        "global_id",
+        "state",
+        "updated_at",
+        "created_without_invite",
+        "group_ids",
+        "id",
+        "profile",
+        "created_at"
+      ]
+    },
+    "MemberInfo": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid"
+        },
+        "is_owner": {
+          "type": "boolean"
+        },
+        "mention_name": {
+          "type": "string"
+        },
+        "name": {
+          "type": "string"
+        },
+        "role": {
+          "type": "string"
+        },
+        "workspace2": {
+          "$ref": "#/definitions/BasicWorkspaceInfo"
+        },
+        "organization2": {
+          "$ref": "#/definitions/MemberInfoOrganization2"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id",
+        "is_owner",
+        "mention_name",
+        "name",
+        "role",
+        "workspace2",
+        "organization2"
+      ]
+    },
+    "MemberInfoOrganization2": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "id"
+      ]
+    },
+    "Milestone": {
+      "description": "(Deprecated) A Milestone is a collection of Epics that represent a release or some other large initiative that you are working on. Milestones have become Objectives, so you should use Objective-related API resources instead of Milestone ones.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Milestone.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The Milestone's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A boolean indicating whether the Milestone has been archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Milestone has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Milestone was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Milestone was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Milestone was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the Milestone.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Milestone has been completed.",
+          "type": "boolean"
+        },
+        "state": {
+          "description": "The workflow state that the Milestone is in.",
+          "type": "string"
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Milestone was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the Milestone was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "categories": {
+          "description": "An array of Categories attached to the Milestone.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Category"
+          }
+        },
+        "id": {
+          "description": "The unique ID of the Milestone.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "key_result_ids": {
+          "description": "The IDs of the Key Results associated with the Objective.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "position": {
+          "description": "A number representing the position of the Milestone in relation to every other Milestone within the Workspace.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "stats": {
+          "$ref": "#/definitions/MilestoneStats"
+        },
+        "created_at": {
+          "description": "The time/date the Milestone was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "started",
+        "entity_type",
+        "completed_at_override",
+        "started_at",
+        "completed_at",
+        "name",
+        "global_id",
+        "completed",
+        "state",
+        "started_at_override",
+        "updated_at",
+        "categories",
+        "id",
+        "key_result_ids",
+        "position",
+        "stats",
+        "created_at"
+      ]
+    },
+    "MilestoneStats": {
+      "description": "A group of calculated values for this Milestone.",
+      "type": "object",
+      "properties": {
+        "average_cycle_time": {
+          "description": "The average cycle time (in seconds) of completed stories in this Milestone.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "average_lead_time": {
+          "description": "The average lead time (in seconds) of completed stories in this Milestone.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_related_documents": {
+          "description": "The number of related documents to this Milestone.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_related_documents"
+      ]
+    },
+    "Objective": {
+      "description": "An Objective is a collection of Epics that represent a release or some other large initiative that you are working on.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Objective.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The Objective's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A boolean indicating whether the Objective has been archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Objective has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Objective was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Objective was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Objective was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the Objective.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Objectivehas been completed.",
+          "type": "boolean"
+        },
+        "state": {
+          "description": "The workflow state that the Objective is in.",
+          "type": "string"
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Objective was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the Objective was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "categories": {
+          "description": "An array of Categories attached to the Objective.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Category"
+          }
+        },
+        "id": {
+          "description": "The unique ID of the Objective.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "key_result_ids": {
+          "description": "The IDs of the Key Results associated with the Objective.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "position": {
+          "description": "A number representing the position of the Objective in relation to every other Objective within the Workspace.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "stats": {
+          "$ref": "#/definitions/ObjectiveStats"
+        },
+        "created_at": {
+          "description": "The time/date the Objective was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "started",
+        "entity_type",
+        "completed_at_override",
+        "started_at",
+        "completed_at",
+        "name",
+        "global_id",
+        "completed",
+        "state",
+        "started_at_override",
+        "updated_at",
+        "categories",
+        "id",
+        "key_result_ids",
+        "position",
+        "stats",
+        "created_at"
+      ]
+    },
+    "ObjectiveSearchResult": {
+      "description": "A Milestone in search results. This is typed differently from Milestone because the details=slim search argument will omit some fields.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Milestone.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The Milestone's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A boolean indicating whether the Milestone has been archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Milestone has been started.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Milestone was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Milestone was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Milestone was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the Milestone.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Milestone has been completed.",
+          "type": "boolean"
+        },
+        "state": {
+          "description": "The workflow state that the Milestone is in.",
+          "type": "string"
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Milestone was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "updated_at": {
+          "description": "The time/date the Milestone was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "categories": {
+          "description": "An array of Categories attached to the Milestone.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Category"
+          }
+        },
+        "id": {
+          "description": "The unique ID of the Milestone.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "key_result_ids": {
+          "description": "The IDs of the Key Results associated with the Objective.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "position": {
+          "description": "A number representing the position of the Milestone in relation to every other Milestone within the Workspace.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "stats": {
+          "$ref": "#/definitions/MilestoneStats"
+        },
+        "created_at": {
+          "description": "The time/date the Milestone was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "archived",
+        "started",
+        "entity_type",
+        "completed_at_override",
+        "started_at",
+        "completed_at",
+        "name",
+        "global_id",
+        "completed",
+        "state",
+        "started_at_override",
+        "updated_at",
+        "categories",
+        "id",
+        "key_result_ids",
+        "position",
+        "stats",
+        "created_at"
+      ]
+    },
+    "ObjectiveSearchResults": {
+      "description": "The results of the Objective search query.",
+      "type": "object",
+      "properties": {
+        "total": {
+          "description": "The total number of matches for the search query. The first 1000 matches can be paged through via the API.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "data": {
+          "description": "A list of search results.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ObjectiveSearchResult"
+          }
+        },
+        "next": {
+          "description": "The URL path and query string for the next page of search results.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "total",
+        "data",
+        "next"
+      ]
+    },
+    "ObjectiveStats": {
+      "description": "A group of calculated values for this Objective.",
+      "type": "object",
+      "properties": {
+        "average_cycle_time": {
+          "description": "The average cycle time (in seconds) of completed stories in this Objective.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "average_lead_time": {
+          "description": "The average lead time (in seconds) of completed stories in this Objective.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_related_documents": {
+          "description": "The number of related documents to this Objective.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_related_documents"
+      ]
+    },
+    "Profile": {
+      "description": "A group of Member profile details.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "deactivated": {
+          "description": "A true/false boolean indicating whether the Member has been deactivated within Shortcut.",
+          "type": "boolean"
+        },
+        "two_factor_auth_activated": {
+          "description": "If Two Factor Authentication is activated for this User.",
+          "type": "boolean"
+        },
+        "mention_name": {
+          "description": "The Member's username within the Organization.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The Member's name within the Organization.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "is_agent": {
+          "description": "Whether this profile is an Agent/Bot user.",
+          "x-doc-skip": true,
+          "type": "boolean"
+        },
+        "gravatar_hash": {
+          "description": "This is the gravatar hash associated with email_address.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique identifier of the profile.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "display_icon": {
+          "$ref": "#/definitions/Icon",
+          "x-nullable": true
+        },
+        "is_owner": {
+          "description": "A boolean indicating whether this profile is an owner at their associated organization.",
+          "type": "boolean"
+        },
+        "email_address": {
+          "description": "The primary email address of the Member with the Organization.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "deactivated",
+        "mention_name",
+        "name",
+        "gravatar_hash",
+        "id",
+        "display_icon",
+        "is_owner",
+        "email_address"
+      ]
+    },
+    "Project": {
+      "description": "Projects typically map to teams (such as Frontend, Backend, Mobile, Devops, etc) but can represent any open-ended product, component, or initiative.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Project.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Project.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "archived": {
+          "description": "True/false boolean indicating whether the Project is in an Archived state.",
+          "type": "boolean"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "days_to_thermometer": {
+          "description": "The number of days before the thermometer appears in the Story summary.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "color": {
+          "description": "The color associated with the Project in the Shortcut member interface.",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "workflow_id": {
+          "description": "The ID of the workflow the project belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "description": "The name of the Project",
+          "type": "string"
+        },
+        "global_id": {
+          "description": "The Global ID of the Project.",
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "start_time": {
+          "description": "The date at which the Project was started.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The time/date that the Project was last updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Project has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "show_thermometer": {
+          "description": "Configuration to enable or disable thermometers in the Story summary.",
+          "type": "boolean"
+        },
+        "team_id": {
+          "description": "The ID of the team the project belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "iteration_length": {
+          "description": "The number of weeks per iteration in this Project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "abbreviation": {
+          "description": "The Project abbreviation used in Story summaries. Should be kept to 3 characters at most.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/ProjectStats"
+        },
+        "created_at": {
+          "description": "The time/date that the Project was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "entity_type",
+        "days_to_thermometer",
+        "color",
+        "workflow_id",
+        "name",
+        "global_id",
+        "start_time",
+        "updated_at",
+        "follower_ids",
+        "external_id",
+        "id",
+        "show_thermometer",
+        "team_id",
+        "iteration_length",
+        "abbreviation",
+        "stats",
+        "created_at"
+      ]
+    },
+    "ProjectStats": {
+      "description": "A group of calculated values for this Project.",
+      "type": "object",
+      "properties": {
+        "num_stories": {
+          "description": "The total number of stories in this Project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_points": {
+          "description": "The total number of points in this Project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_related_documents": {
+          "description": "The total number of documents related to this Project",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_stories",
+        "num_points",
+        "num_related_documents"
+      ]
+    },
+    "PullRequest": {
+      "description": "Corresponds to a VCS Pull Request attached to a Shortcut story.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "closed": {
+          "description": "True/False boolean indicating whether the VCS pull request has been closed.",
+          "type": "boolean"
+        },
+        "merged": {
+          "description": "True/False boolean indicating whether the VCS pull request has been merged.",
+          "type": "boolean"
+        },
+        "num_added": {
+          "description": "Number of lines added in the pull request, according to VCS.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "branch_id": {
+          "description": "The ID of the branch for the particular pull request.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "overlapping_stories": {
+          "description": "An array of Story ids that have Pull Requests that change at least one of the same lines this Pull Request changes.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "number": {
+          "description": "The pull request's unique number ID in VCS.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "branch_name": {
+          "description": "The name of the branch for the particular pull request.",
+          "type": "string"
+        },
+        "target_branch_name": {
+          "description": "The name of the target branch for the particular pull request.",
+          "type": "string"
+        },
+        "num_commits": {
+          "description": "The number of commits on the pull request.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "title": {
+          "description": "The title of the pull request.",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date the pull request was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "has_overlapping_stories": {
+          "description": "Boolean indicating that the Pull Request has Stories that have Pull Requests that change at least one of the same lines this Pull Request changes.",
+          "type": "boolean"
+        },
+        "draft": {
+          "description": "True/False boolean indicating whether the VCS pull request is in the draft state.",
+          "type": "boolean"
+        },
+        "id": {
+          "description": "The unique ID associated with the pull request in Shortcut.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "vcs_labels": {
+          "description": "An array of PullRequestLabels attached to the PullRequest.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/PullRequestLabel"
+          },
+          "x-nullable": true
+        },
+        "url": {
+          "description": "The URL for the pull request.",
+          "type": "string"
+        },
+        "num_removed": {
+          "description": "Number of lines removed in the pull request, according to VCS.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "review_status": {
+          "description": "The status of the review for the pull request.",
+          "type": "string"
+        },
+        "num_modified": {
+          "description": "Number of lines modified in the pull request, according to VCS.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "build_status": {
+          "description": "The status of the Continuous Integration workflow for the pull request.",
+          "type": "string"
+        },
+        "target_branch_id": {
+          "description": "The ID of the target branch for the particular pull request.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "repository_id": {
+          "description": "The ID of the repository for the particular pull request.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the pull request was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "closed",
+        "merged",
+        "num_added",
+        "branch_id",
+        "number",
+        "branch_name",
+        "target_branch_name",
+        "num_commits",
+        "title",
+        "updated_at",
+        "has_overlapping_stories",
+        "draft",
+        "id",
+        "url",
+        "num_removed",
+        "num_modified",
+        "target_branch_id",
+        "repository_id",
+        "created_at"
+      ]
+    },
+    "PullRequestLabel": {
+      "description": "Corresponds to a VCS Label associated with a Pull Request.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "id": {
+          "description": "The unique ID of the VCS Label.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The color of the VCS label.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the VCS label.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the VCS label.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "id",
+        "color",
+        "name"
+      ]
+    },
+    "RemoveCustomFieldParams": {
+      "type": "object",
+      "properties": {
+        "field_id": {
+          "description": "The unique public ID for the CustomField.",
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "field_id"
+      ]
+    },
+    "RemoveLabelParams": {
+      "description": "Request parameters for removing a Label from a Shortcut Story.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The name of the new Label to remove.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "name"
+      ]
+    },
+    "Repository": {
+      "description": "Repository refers to a VCS repository.",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The shorthand name of the VCS repository.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "type": {
+          "description": "The VCS provider for the Repository.",
+          "type": "string",
+          "enum": [
+            "github",
+            "gitlab",
+            "bitbucket"
+          ]
+        },
+        "updated_at": {
+          "description": "The time/date the Repository was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "external_id": {
+          "description": "The VCS unique identifier for the Repository.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The ID associated to the VCS repository in Shortcut.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "url": {
+          "description": "The URL of the Repository.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "full_name": {
+          "description": "The full name of the VCS repository.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "created_at": {
+          "description": "The time/date the Repository was created.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "name",
+        "type",
+        "updated_at",
+        "external_id",
+        "id",
+        "url",
+        "full_name",
+        "created_at"
+      ]
+    },
+    "SearchResults": {
+      "description": "The results of the multi-entity search query.",
+      "type": "object",
+      "properties": {
+        "epics": {
+          "$ref": "#/definitions/EpicSearchResults"
+        },
+        "stories": {
+          "$ref": "#/definitions/StorySearchResults"
+        },
+        "iterations": {
+          "$ref": "#/definitions/IterationSearchResults"
+        },
+        "milestones": {
+          "$ref": "#/definitions/ObjectiveSearchResults"
+        }
+      },
+      "additionalProperties": false
+    },
+    "SearchStories": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "archived": {
+          "description": "A true/false boolean indicating whether the Story is in archived state.",
+          "type": "boolean"
+        },
+        "owner_id": {
+          "description": "An array of UUIDs for any Users who may be Owners of the Stories.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "story_type": {
+          "description": "The type of Stories that you want returned.",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "epic_ids": {
+          "description": "The Epic IDs that may be associated with the Stories.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "project_ids": {
+          "description": "The IDs for the Projects the Stories may be assigned to.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64",
+            "x-nullable": true
+          },
+          "uniqueItems": true
+        },
+        "updated_at_end": {
+          "description": "Stories should have been updated on or before this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "completed_at_end": {
+          "description": "Stories should have been completed on or before this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "workflow_state_types": {
+          "description": "The type of Workflow State the Stories may be in.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "enum": [
+              "started",
+              "backlog",
+              "unstarted",
+              "done"
+            ]
+          }
+        },
+        "deadline_end": {
+          "description": "Stories should have a deadline on or before this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "created_at_start": {
+          "description": "Stories should have been created on or after this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "epic_id": {
+          "description": "The Epic IDs that may be associated with the Stories.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "label_name": {
+          "minLength": 1,
+          "description": "The name of any associated Labels.",
+          "type": "string"
+        },
+        "requested_by_id": {
+          "description": "The UUID of any Users who may have requested the Stories.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The Iteration ID that may be associated with the Stories.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "label_ids": {
+          "description": "The Label IDs that may be associated with the Stories.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "group_id": {
+          "description": "The Group ID that is associated with the Stories",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The unique IDs of the specific Workflow States that the Stories should be in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "iteration_ids": {
+          "description": "The Iteration IDs that may be associated with the Stories.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "created_at_end": {
+          "description": "Stories should have been created on or before this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "deadline_start": {
+          "description": "Stories should have a deadline on or after this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_ids": {
+          "description": "The Group IDs that are associated with the Stories",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any Users who may be Owners of the Stories.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "external_id": {
+          "maxLength": 1024,
+          "description": "An ID or URL that references an external resource. Useful during imports.",
+          "type": "string"
+        },
+        "includes_description": {
+          "description": "Whether to include the story description in the response.",
+          "type": "boolean"
+        },
+        "estimate": {
+          "description": "The number of estimate points associate with the Stories.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "project_id": {
+          "description": "The IDs for the Projects the Stories may be assigned to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "completed_at_start": {
+          "description": "Stories should have been completed on or after this date.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at_start": {
+          "description": "Stories should have been updated on or after this date.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false
+    },
+    "Story": {
+      "description": "Stories are the standard unit of work in Shortcut and represent individual features, bugs, and chores.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Story.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True if the story has been archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Story has been started.",
+          "type": "boolean"
+        },
+        "story_links": {
+          "description": "An array of story links attached to the Story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/TypedStoryLink"
+          }
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "synced_item": {
+          "$ref": "#/definitions/SyncedItem"
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string"
+        },
+        "custom_fields": {
+          "description": "An array of CustomField value assertions for the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryCustomField"
+          }
+        },
+        "linked_files": {
+          "description": "An array of linked files attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LinkedFile"
+          }
+        },
+        "workflow_id": {
+          "description": "The ID of the workflow the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Story has been completed.",
+          "type": "boolean"
+        },
+        "comments": {
+          "description": "An array of comments attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryComment"
+          }
+        },
+        "blocker": {
+          "description": "A true/false boolean indicating if the Story is currently a blocker of another story.",
+          "type": "boolean"
+        },
+        "branches": {
+          "description": "An array of Git branches attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Branch"
+          }
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "story_template_id": {
+          "description": "The ID of the story template used to create this story, or null if not created using a template.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of external links (strings) associated with a Story",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "previous_iteration_ids": {
+          "description": "The IDs of the iteration the story belongs to.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "sub_task_story_ids": {
+          "description": "The Story IDs of Sub-tasks attached to the Story\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "tasks": {
+          "description": "An array of tasks connected to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Task"
+          }
+        },
+        "formatted_vcs_branch_name": {
+          "description": "The formatted branch name for this story.",
+          "x-doc-skip": true,
+          "type": "string",
+          "x-nullable": true
+        },
+        "label_ids": {
+          "description": "An array of label ids attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "The ID of the group associated with the story.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story is currently in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The time/date the Story was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "pull_requests": {
+          "description": "An array of Pull/Merge Requests attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/PullRequest"
+          }
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Story has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "lead_time": {
+          "description": "The lead time (in seconds) of this story when complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "parent_story_id": {
+          "description": "The ID of the parent story to this story (making this story a sub-task).\nField only applicable when Sub-task feature is enabled.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "commits": {
+          "description": "An array of commits attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Commit"
+          }
+        },
+        "files": {
+          "description": "An array of files attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/UploadedFile"
+          }
+        },
+        "position": {
+          "description": "A number representing the position of the story in relation to every other story in the current project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "blocked": {
+          "description": "A true/false boolean indicating if the Story is currently blocked.",
+          "type": "boolean"
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/StoryStats"
+        },
+        "cycle_time": {
+          "description": "The cycle time (in seconds) of this story when complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Story was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "moved_at": {
+          "description": "The time/date the Story was last changed workflow-state.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "description",
+        "archived",
+        "started",
+        "story_links",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "story_type",
+        "linked_files",
+        "workflow_id",
+        "completed_at_override",
+        "started_at",
+        "completed_at",
+        "name",
+        "global_id",
+        "completed",
+        "comments",
+        "blocker",
+        "branches",
+        "epic_id",
+        "story_template_id",
+        "external_links",
+        "previous_iteration_ids",
+        "requested_by_id",
+        "iteration_id",
+        "tasks",
+        "label_ids",
+        "started_at_override",
+        "group_id",
+        "workflow_state_id",
+        "updated_at",
+        "pull_requests",
+        "group_mention_ids",
+        "follower_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "estimate",
+        "commits",
+        "files",
+        "position",
+        "blocked",
+        "project_id",
+        "deadline",
+        "stats",
+        "created_at",
+        "moved_at"
+      ]
+    },
+    "StoryComment": {
+      "description": "A Comment is any note added within the Comment field of a Story.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Comment.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "deleted": {
+          "description": "True/false boolean indicating whether the Comment has been deleted.",
+          "type": "boolean"
+        },
+        "story_id": {
+          "description": "The ID of the Story on which the Comment appears.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "author_id": {
+          "description": "The unique ID of the Member who is the Comment's author.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "member_mention_ids": {
+          "description": "The unique IDs of the Member who are mentioned in the Comment.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "blocker": {
+          "description": "Marks the comment as a blocker that can be surfaced to permissions or teams mentioned in the comment. Can only be used on a top-level comment.",
+          "type": "boolean"
+        },
+        "linked_to_slack": {
+          "description": "Whether the Comment is currently the root of a thread that is linked to Slack.",
+          "type": "boolean"
+        },
+        "updated_at": {
+          "description": "The time/date when the Comment was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_mention_ids": {
+          "description": "The unique IDs of the Group who are mentioned in the Comment.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Comment has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "parent_id": {
+          "description": "The ID of the parent Comment this Comment is threaded under.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Comment.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "position": {
+          "description": "The Comments numerical position in the list from oldest to newest.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "unblocks_parent": {
+          "description": "Marks the comment as an unblocker to its  blocker parent. Can only be set on a threaded comment who has a parent with `blocker` set.",
+          "type": "boolean"
+        },
+        "reactions": {
+          "description": "A set of Reactions to this Comment.",
+          "x-doc-skip": true,
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryReaction"
+          }
+        },
+        "created_at": {
+          "description": "The time/date when the Comment was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "text": {
+          "description": "The text of the Comment. In the case that the Comment has been deleted, this field can be set to nil.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "entity_type",
+        "deleted",
+        "story_id",
+        "mention_ids",
+        "author_id",
+        "member_mention_ids",
+        "linked_to_slack",
+        "updated_at",
+        "group_mention_ids",
+        "external_id",
+        "id",
+        "position",
+        "reactions",
+        "created_at",
+        "text"
+      ]
+    },
+    "StoryContents": {
+      "description": "A container entity for the attributes this template should populate.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string"
+        },
+        "custom_fields": {
+          "description": "An array of maps specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "linked_files": {
+          "description": "An array of linked files attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LinkedFile"
+          }
+        },
+        "name": {
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "external_links": {
+          "description": "An array of external links connected to the story.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "sub_tasks": {
+          "description": "An array of maps specifying the sub-tasks to create and link to the story.\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateSubTaskParams"
+          }
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "tasks": {
+          "description": "An array of tasks connected to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryContentsTask"
+          }
+        },
+        "label_ids": {
+          "description": "An array of label ids attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "group_id": {
+          "description": "The ID of the group to which the story is assigned.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story is currently in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "files": {
+          "description": "An array of files attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/UploadedFile"
+          }
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryContentsTask": {
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "Full text of the Task.",
+          "type": "string"
+        },
+        "position": {
+          "description": "The number corresponding to the Task's position within a list of Tasks on a Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "complete": {
+          "description": "True/false boolean indicating whether the Task has been completed.",
+          "type": "boolean"
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the Owners of this Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Task has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description"
+      ]
+    },
+    "StoryCustomField": {
+      "type": "object",
+      "properties": {
+        "field_id": {
+          "description": "The unique public ID for a CustomField.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "value_id": {
+          "description": "The unique public ID for a CustomFieldEnumValue.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "value": {
+          "description": "A string representation of the value, if applicable.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "field_id",
+        "value_id",
+        "value"
+      ]
+    },
+    "StoryHistoryChangeAddsRemovesInt": {
+      "description": "Task IDs that have been added or removed from the Story.",
+      "type": "object",
+      "properties": {
+        "adds": {
+          "description": "The values that have been added.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "removes": {
+          "description": "The values that have been removed",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryHistoryChangeAddsRemovesUuid": {
+      "description": "Custom Field Enum Value IDs that have been added or removed from the Story.",
+      "type": "object",
+      "properties": {
+        "adds": {
+          "description": "The values that have been added.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "removes": {
+          "description": "The values that have been removed",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryHistoryChangeOldNewBool": {
+      "description": "True if the Story has archived, otherwise false.",
+      "type": "object",
+      "properties": {
+        "old": {
+          "description": "The old value.",
+          "type": "boolean"
+        },
+        "new": {
+          "description": "The new value.",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryHistoryChangeOldNewInt": {
+      "description": "The estimate value for the Story",
+      "type": "object",
+      "properties": {
+        "old": {
+          "description": "The old value.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "new": {
+          "description": "The new value.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryHistoryChangeOldNewStr": {
+      "description": "A timestamp that represents the Story's deadline.",
+      "type": "object",
+      "properties": {
+        "old": {
+          "description": "The old value.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "new": {
+          "description": "The new value.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryHistoryChangeOldNewUuid": {
+      "description": "The Team ID for the Story.",
+      "type": "object",
+      "properties": {
+        "old": {
+          "description": "The old value.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "new": {
+          "description": "The new value.",
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false
+    },
+    "StoryLink": {
+      "description": "Story links allow you create semantic relationships between two stories. Relationship types are relates to, blocks / blocked by, and duplicates / is duplicated by. The format is `subject -> link -> object`, or for example \"story 5 blocks story 6\".",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "id": {
+          "description": "The unique identifier of the Story Link.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "subject_id": {
+          "description": "The ID of the subject Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "subject_workflow_state_id": {
+          "description": "The workflow state of the \"subject\" story.",
+          "x-doc-skip": true,
+          "type": "integer",
+          "format": "int64"
+        },
+        "verb": {
+          "description": "How the subject Story acts on the object Story. This can be \"blocks\", \"duplicates\", or \"relates to\".",
+          "type": "string"
+        },
+        "object_id": {
+          "description": "The ID of the object Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date when the Story Link was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The time/date when the Story Link was last updated.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "id",
+        "subject_id",
+        "subject_workflow_state_id",
+        "verb",
+        "object_id",
+        "created_at",
+        "updated_at"
+      ]
+    },
+    "StoryReaction": {
+      "description": "Emoji reaction on a comment.",
+      "type": "object",
+      "properties": {
+        "emoji": {
+          "description": "Emoji text of the reaction.",
+          "type": "string"
+        },
+        "permission_ids": {
+          "description": "Permissions who have reacted with this.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "emoji",
+        "permission_ids"
+      ]
+    },
+    "StorySearchResult": {
+      "description": "A Story in search results. This is typed differently from Story because the details=slim search argument will omit some fields.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Story.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True if the story has been archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Story has been started.",
+          "type": "boolean"
+        },
+        "story_links": {
+          "description": "An array of story links attached to the Story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/TypedStoryLink"
+          }
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "task_ids": {
+          "description": "An array of IDs of Tasks attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "synced_item": {
+          "$ref": "#/definitions/SyncedItem"
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string"
+        },
+        "custom_fields": {
+          "description": "An array of CustomField value assertions for the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryCustomField"
+          }
+        },
+        "linked_files": {
+          "description": "An array of linked files attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LinkedFile"
+          }
+        },
+        "file_ids": {
+          "description": "An array of IDs of Files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "num_tasks_completed": {
+          "description": "The number of tasks on the story which are complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "workflow_id": {
+          "description": "The ID of the workflow the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Story has been completed.",
+          "type": "boolean"
+        },
+        "comments": {
+          "description": "An array of comments attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryComment"
+          }
+        },
+        "blocker": {
+          "description": "A true/false boolean indicating if the Story is currently a blocker of another story.",
+          "type": "boolean"
+        },
+        "branches": {
+          "description": "An array of Git branches attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Branch"
+          }
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "story_template_id": {
+          "description": "The ID of the story template used to create this story, or null if not created using a template.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of external links (strings) associated with a Story",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "previous_iteration_ids": {
+          "description": "The IDs of the iteration the story belongs to.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "sub_task_story_ids": {
+          "description": "The Story IDs of Sub-tasks attached to the Story\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "tasks": {
+          "description": "An array of tasks connected to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Task"
+          }
+        },
+        "formatted_vcs_branch_name": {
+          "description": "The formatted branch name for this story.",
+          "x-doc-skip": true,
+          "type": "string",
+          "x-nullable": true
+        },
+        "label_ids": {
+          "description": "An array of label ids attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "The ID of the group associated with the story.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story is currently in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The time/date the Story was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "pull_requests": {
+          "description": "An array of Pull/Merge Requests attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/PullRequest"
+          }
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Story has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "lead_time": {
+          "description": "The lead time (in seconds) of this story when complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "parent_story_id": {
+          "description": "The ID of the parent story to this story (making this story a sub-task).\nField only applicable when Sub-task feature is enabled.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "commits": {
+          "description": "An array of commits attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Commit"
+          }
+        },
+        "files": {
+          "description": "An array of files attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/UploadedFile"
+          }
+        },
+        "position": {
+          "description": "A number representing the position of the story in relation to every other story in the current project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "blocked": {
+          "description": "A true/false boolean indicating if the Story is currently blocked.",
+          "type": "boolean"
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "linked_file_ids": {
+          "description": "An array of IDs of LinkedFiles attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/StoryStats"
+        },
+        "comment_ids": {
+          "description": "An array of IDs of Comments attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "cycle_time": {
+          "description": "The cycle time (in seconds) of this story when complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Story was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "moved_at": {
+          "description": "The time/date the Story was last changed workflow-state.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "archived",
+        "started",
+        "story_links",
+        "entity_type",
+        "labels",
+        "mention_ids",
+        "member_mention_ids",
+        "story_type",
+        "workflow_id",
+        "completed_at_override",
+        "started_at",
+        "completed_at",
+        "name",
+        "global_id",
+        "completed",
+        "blocker",
+        "epic_id",
+        "story_template_id",
+        "external_links",
+        "previous_iteration_ids",
+        "requested_by_id",
+        "iteration_id",
+        "label_ids",
+        "started_at_override",
+        "group_id",
+        "workflow_state_id",
+        "updated_at",
+        "group_mention_ids",
+        "follower_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "estimate",
+        "position",
+        "blocked",
+        "project_id",
+        "deadline",
+        "stats",
+        "created_at",
+        "moved_at"
+      ]
+    },
+    "StorySearchResults": {
+      "description": "The results of the Story search query.",
+      "type": "object",
+      "properties": {
+        "total": {
+          "description": "The total number of matches for the search query. The first 1000 matches can be paged through via the API.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "data": {
+          "description": "A list of search results.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StorySearchResult"
+          }
+        },
+        "next": {
+          "description": "The URL path and query string for the next page of search results.",
+          "type": "string",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "total",
+        "data",
+        "next"
+      ]
+    },
+    "StorySlim": {
+      "description": "StorySlim represents the same resource as a Story, but is more light-weight. For certain fields it provides ids rather than full resources (e.g., `comment_ids` and `file_ids`) and it also excludes certain aggregate values (e.g., `cycle_time`). The `description` field can be optionally included. Use the [Get Story](#Get-Story) endpoint to fetch the unabridged payload for a Story.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Story.",
+          "type": "string"
+        },
+        "description": {
+          "description": "The description of the Story.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True if the story has been archived or not.",
+          "type": "boolean"
+        },
+        "started": {
+          "description": "A true/false boolean indicating if the Story has been started.",
+          "type": "boolean"
+        },
+        "story_links": {
+          "description": "An array of story links attached to the Story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/TypedStoryLink"
+          }
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LabelSlim"
+          }
+        },
+        "task_ids": {
+          "description": "An array of IDs of Tasks attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "synced_item": {
+          "$ref": "#/definitions/SyncedItem"
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string"
+        },
+        "custom_fields": {
+          "description": "An array of CustomField value assertions for the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/StoryCustomField"
+          }
+        },
+        "file_ids": {
+          "description": "An array of IDs of Files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "num_tasks_completed": {
+          "description": "The number of tasks on the story which are complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "workflow_id": {
+          "description": "The ID of the workflow the story belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "started_at": {
+          "description": "The time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "completed_at": {
+          "description": "The time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "completed": {
+          "description": "A true/false boolean indicating if the Story has been completed.",
+          "type": "boolean"
+        },
+        "blocker": {
+          "description": "A true/false boolean indicating if the Story is currently a blocker of another story.",
+          "type": "boolean"
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "story_template_id": {
+          "description": "The ID of the story template used to create this story, or null if not created using a template.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of external links (strings) associated with a Story",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "previous_iteration_ids": {
+          "description": "The IDs of the iteration the story belongs to.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "requested_by_id": {
+          "description": "The ID of the Member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "sub_task_story_ids": {
+          "description": "The Story IDs of Sub-tasks attached to the Story\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "formatted_vcs_branch_name": {
+          "description": "The formatted branch name for this story.",
+          "x-doc-skip": true,
+          "type": "string",
+          "x-nullable": true
+        },
+        "label_ids": {
+          "description": "An array of label ids attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "The ID of the group associated with the story.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state the story is currently in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "updated_at": {
+          "description": "The time/date the Story was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in the Story description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Story has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "lead_time": {
+          "description": "The lead time (in seconds) of this story when complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "parent_story_id": {
+          "description": "The ID of the parent story to this story (making this story a sub-task).\nField only applicable when Sub-task feature is enabled.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "position": {
+          "description": "A number representing the position of the story in relation to every other story in the current project.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "blocked": {
+          "description": "A true/false boolean indicating if the Story is currently blocked.",
+          "type": "boolean"
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "linked_file_ids": {
+          "description": "An array of IDs of LinkedFiles attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "stats": {
+          "$ref": "#/definitions/StoryStats"
+        },
+        "comment_ids": {
+          "description": "An array of IDs of Comments attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "cycle_time": {
+          "description": "The cycle time (in seconds) of this story when complete.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Story was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "moved_at": {
+          "description": "The time/date the Story was last changed workflow-state.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "archived",
+        "started",
+        "story_links",
+        "entity_type",
+        "labels",
+        "task_ids",
+        "mention_ids",
+        "member_mention_ids",
+        "story_type",
+        "file_ids",
+        "num_tasks_completed",
+        "workflow_id",
+        "completed_at_override",
+        "started_at",
+        "completed_at",
+        "name",
+        "global_id",
+        "completed",
+        "blocker",
+        "epic_id",
+        "story_template_id",
+        "external_links",
+        "previous_iteration_ids",
+        "requested_by_id",
+        "iteration_id",
+        "label_ids",
+        "started_at_override",
+        "group_id",
+        "workflow_state_id",
+        "updated_at",
+        "group_mention_ids",
+        "follower_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "estimate",
+        "position",
+        "blocked",
+        "project_id",
+        "linked_file_ids",
+        "deadline",
+        "stats",
+        "comment_ids",
+        "created_at",
+        "moved_at"
+      ]
+    },
+    "StoryStats": {
+      "description": "The stats object for Stories",
+      "type": "object",
+      "properties": {
+        "num_related_documents": {
+          "description": "The number of documents related to this Story.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "num_related_documents"
+      ]
+    },
+    "SyncedItem": {
+      "x-doc-skip": true,
+      "description": "The synced item for the story.",
+      "type": "object",
+      "properties": {
+        "external_id": {
+          "description": "The id used to reference an external entity.",
+          "type": "string"
+        },
+        "url": {
+          "description": "The url to the external entity.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "external_id",
+        "url"
+      ]
+    },
+    "Task": {
+      "description": "A Task on a Story.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "Full text of the Task.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "story_id": {
+          "description": "The unique identifier of the parent Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "An array of UUIDs of Members mentioned in this Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "completed_at": {
+          "description": "The time/date the Task was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date the Task was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_mention_ids": {
+          "description": "An array of UUIDs of Groups mentioned in this Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the Owners of this Task.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Task has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Task.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "position": {
+          "description": "The number corresponding to the Task's position within a list of Tasks on a Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "complete": {
+          "description": "True/false boolean indicating whether the Task has been completed.",
+          "type": "boolean"
+        },
+        "created_at": {
+          "description": "The time/date the Task was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "story_id",
+        "mention_ids",
+        "member_mention_ids",
+        "completed_at",
+        "global_id",
+        "updated_at",
+        "group_mention_ids",
+        "owner_ids",
+        "external_id",
+        "id",
+        "position",
+        "complete",
+        "created_at"
+      ]
+    },
+    "ThreadedComment": {
+      "description": "Comments associated with Epic Discussions.",
+      "type": "object",
+      "properties": {
+        "app_url": {
+          "description": "The Shortcut application url for the Comment.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "deleted": {
+          "description": "True/false boolean indicating whether the Comment is deleted.",
+          "type": "boolean"
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "author_id": {
+          "description": "The unique ID of the Member that authored the Comment.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "member_mention_ids": {
+          "description": "An array of Member IDs that have been mentioned in this Comment.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "comments": {
+          "description": "A nested array of threaded comments.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ThreadedComment"
+          }
+        },
+        "updated_at": {
+          "description": "The time/date the Comment was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "group_mention_ids": {
+          "description": "An array of Group IDs that have been mentioned in this Comment.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the Comment has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID of the Comment.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Comment was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "text": {
+          "description": "The text of the Comment.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "app_url",
+        "entity_type",
+        "deleted",
+        "mention_ids",
+        "author_id",
+        "member_mention_ids",
+        "comments",
+        "updated_at",
+        "group_mention_ids",
+        "external_id",
+        "id",
+        "created_at",
+        "text"
+      ]
+    },
+    "TypedStoryLink": {
+      "description": "The type of Story Link. The string can be subject or object. ",
+      "type": "object",
+      "properties": {
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "object_id": {
+          "description": "The ID of the object Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "verb": {
+          "description": "How the subject Story acts on the object Story. This can be \"blocks\", \"duplicates\", or \"relates to\".",
+          "type": "string"
+        },
+        "type": {
+          "description": "This indicates whether the Story is the subject or object in the Story Link.",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date when the Story Link was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "id": {
+          "description": "The unique identifier of the Story Link.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "subject_id": {
+          "description": "The ID of the subject Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "subject_workflow_state_id": {
+          "description": "The workflow state of the \"subject\" story.",
+          "x-doc-skip": true,
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date when the Story Link was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "entity_type",
+        "object_id",
+        "verb",
+        "type",
+        "updated_at",
+        "id",
+        "subject_id",
+        "subject_workflow_state_id",
+        "created_at"
+      ]
+    },
+    "UnprocessableError": {
+      "type": "object",
+      "properties": {
+        "message": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "message"
+      ]
+    },
+    "UnusableEntitlementError": {
+      "type": "object",
+      "properties": {
+        "reason_tag": {
+          "description": "The tag for violating an entitlement action.",
+          "type": "string",
+          "enum": [
+            "entitlement-violation"
+          ]
+        },
+        "entitlement_tag": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "Short tag describing the unusable entitlement action taken by the user.",
+          "type": "string"
+        },
+        "message": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "Message displayed to the user on why their action failed.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "reason_tag",
+        "entitlement_tag",
+        "message"
+      ]
+    },
+    "UpdateCategory": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The new name of the Category.",
+          "type": "string"
+        },
+        "color": {
+          "description": "The hex color to be displayed with the Category (for example, \"#ff0000\").",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "archived": {
+          "description": "A true/false boolean indicating if the Category has been archived.",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateComment": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "text": {
+          "description": "The updated comment text.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "text"
+      ]
+    },
+    "UpdateCustomField": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "description": "Indicates whether the Field is enabled for the Workspace. Only enabled fields can be applied to Stories.",
+          "type": "boolean"
+        },
+        "name": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "A collection of objects representing reporting periods for years.",
+          "type": "string"
+        },
+        "values": {
+          "description": "A collection of EnumValue objects representing the values in the domain of some Custom Field.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/UpdateCustomFieldEnumValue"
+          }
+        },
+        "icon_set_identifier": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "A frontend-controlled string that represents the icon for this custom field.",
+          "type": "string"
+        },
+        "description": {
+          "description": "A description of the purpose of this field.",
+          "type": "string"
+        },
+        "before_id": {
+          "description": "The ID of the CustomField we want to move this CustomField before.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "after_id": {
+          "description": "The ID of the CustomField we want to move this CustomField after.",
+          "type": "string",
+          "format": "uuid"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateCustomFieldEnumValue": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "The unique ID of an existing EnumValue within the CustomField's domain.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "value": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "A string value within the domain of this Custom Field.",
+          "type": "string"
+        },
+        "color_key": {
+          "description": "A color key associated with this EnumValue within the CustomField's domain.",
+          "type": "string",
+          "enum": [
+            "blue",
+            "purple",
+            "midnight-blue",
+            "orange",
+            "yellow-green",
+            "brass",
+            "gray",
+            "fuchsia",
+            "yellow",
+            "pink",
+            "sky-blue",
+            "green",
+            "red",
+            "black",
+            "slate",
+            "turquoise"
+          ],
+          "x-nullable": true
+        },
+        "enabled": {
+          "description": "Whether this EnumValue is enabled for its CustomField or not. Leaving this key out of the request leaves the current enabled state untouched.",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateDoc": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "title": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The new title for the document",
+          "type": "string"
+        },
+        "content": {
+          "description": "The new content for the document.",
+          "type": "string"
+        },
+        "content_format": {
+          "description": "Format of content. For input: specifies format of provided content (defaults to 'html'). For output: controls response format - 'markdown' (default) or 'html' to include HTML content.",
+          "type": "string",
+          "enum": [
+            "markdown",
+            "html"
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateEntityTemplate": {
+      "description": "Request parameters for changing either a template's name or any of\n  the attributes it is designed to pre-populate.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The updated template name.",
+          "type": "string"
+        },
+        "story_contents": {
+          "$ref": "#/definitions/UpdateStoryContents"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateEpic": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The Epic's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A true/false boolean indicating whether the Epic is in archived state.",
+          "type": "boolean"
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Epic.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Epic was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "objective_ids": {
+          "description": "An array of IDs for Objectives to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The Epic's name.",
+          "type": "string"
+        },
+        "planned_start_date": {
+          "description": "The Epic's planned start date.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "state": {
+          "description": "`Deprecated` The Epic's state (to do, in progress, or done); will be ignored when `epic_state_id` is set.",
+          "type": "string",
+          "enum": [
+            "in progress",
+            "to do",
+            "done"
+          ]
+        },
+        "milestone_id": {
+          "description": "`Deprecated` The ID of the Milestone this Epic is related to. Use `objective_ids`.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the member that requested the epic.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "epic_state_id": {
+          "description": "The ID of the Epic State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Epic was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "`Deprecated` The ID of the group to associate with the epic. Use `group_ids`.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers on this Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDS for Groups to which this Epic is related.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs for any members you want to add as Owners on this Epic.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "This field can be set to another unique ID. In the case that the Epic has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string"
+        },
+        "before_id": {
+          "description": "The ID of the Epic we want to move this Epic before.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "after_id": {
+          "description": "The ID of the Epic we want to move this Epic after.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "deadline": {
+          "description": "The Epic's deadline.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateFile": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 4096,
+          "description": "The description of the file.",
+          "type": "string"
+        },
+        "created_at": {
+          "description": "The time/date that the file was uploaded.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "description": "The time/date that the file was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "name": {
+          "maxLength": 1024,
+          "minLength": 1,
+          "description": "The name of the file.",
+          "type": "string"
+        },
+        "uploader_id": {
+          "description": "The unique ID assigned to the Member who uploaded the file to Shortcut.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "external_id": {
+          "maxLength": 128,
+          "description": "An additional ID that you may wish to assign to the file.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateGroup": {
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 4096,
+          "description": "The description of this Group.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "Whether or not this Group is archived.",
+          "type": "boolean",
+          "x-nullable": true
+        },
+        "color": {
+          "description": "The color you wish to use for the Group in the system.",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "display_icon_id": {
+          "description": "The Icon id for the avatar of this Group.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "mention_name": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "The mention name of this Group.",
+          "type": "string"
+        },
+        "name": {
+          "maxLength": 63,
+          "minLength": 1,
+          "description": "The name of this Group.",
+          "type": "string"
+        },
+        "color_key": {
+          "description": "The color key you wish to use for the Group in the system.",
+          "type": "string",
+          "enum": [
+            "blue",
+            "purple",
+            "midnight-blue",
+            "orange",
+            "yellow-green",
+            "brass",
+            "gray",
+            "fuchsia",
+            "yellow",
+            "pink",
+            "sky-blue",
+            "green",
+            "red",
+            "black",
+            "slate",
+            "turquoise"
+          ]
+        },
+        "default_workflow_id": {
+          "description": "The ID of the default workflow for stories created in this group.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "member_ids": {
+          "description": "The Member ids to add to this Group.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "workflow_ids": {
+          "description": "The Workflow ids to add to the Group.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateHealth": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "status": {
+          "description": "The health status of the Epic.",
+          "type": "string",
+          "enum": [
+            "At Risk",
+            "On Track",
+            "Off Track",
+            "No Health"
+          ]
+        },
+        "text": {
+          "description": "The description of the Health status.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateIteration": {
+      "type": "object",
+      "properties": {
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "group_ids": {
+          "description": "An array of UUIDs for any Groups you want to add as Followers. Currently, only one Group association is presented in our web UI.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "labels": {
+          "description": "An array of Labels attached to the Iteration.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "description": {
+          "maxLength": 100000,
+          "description": "The description of the Iteration.",
+          "type": "string"
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of this Iteration",
+          "type": "string"
+        },
+        "start_date": {
+          "minLength": 1,
+          "description": "The date this Iteration begins, e.g. 2019-07-01",
+          "type": "string"
+        },
+        "end_date": {
+          "minLength": 1,
+          "description": "The date this Iteration ends, e.g. 2019-07-05.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateKeyResult": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 1024,
+          "description": "The name of the Key Result.",
+          "type": "string"
+        },
+        "initial_observed_value": {
+          "$ref": "#/definitions/KeyResultValue"
+        },
+        "observed_value": {
+          "$ref": "#/definitions/KeyResultValue"
+        },
+        "target_value": {
+          "$ref": "#/definitions/KeyResultValue"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateLabel": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The new name of the label.",
+          "type": "string"
+        },
+        "description": {
+          "maxLength": 1024,
+          "description": "The new description of the label.",
+          "type": "string"
+        },
+        "color": {
+          "description": "The hex color to be displayed with the Label (for example, \"#ff0000\").",
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "minLength": 1,
+          "type": "string",
+          "x-nullable": true
+        },
+        "archived": {
+          "description": "A true/false boolean indicating if the Label has been archived.",
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateLinkedFile": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 512,
+          "description": "The description of the file.",
+          "type": "string"
+        },
+        "story_id": {
+          "description": "The ID of the linked story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "minLength": 1,
+          "description": "The name of the file.",
+          "type": "string"
+        },
+        "thumbnail_url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The URL of the thumbnail, if the integration provided it.",
+          "type": "string"
+        },
+        "type": {
+          "description": "The integration type of the file (e.g. google, dropbox, box).",
+          "type": "string",
+          "enum": [
+            "google",
+            "url",
+            "dropbox",
+            "box",
+            "onedrive"
+          ]
+        },
+        "size": {
+          "description": "The filesize, if the integration provided it.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "uploader_id": {
+          "description": "The UUID of the member that uploaded the file.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "url": {
+          "pattern": "^https?://.+$",
+          "maxLength": 2048,
+          "description": "The URL of linked file.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateMilestone": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The Milestone's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A boolean indicating whether the Milestone is archived or not",
+          "type": "boolean"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Milestone was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of the Milestone.",
+          "type": "string"
+        },
+        "state": {
+          "description": "The workflow state that the Milestone is in.",
+          "type": "string",
+          "enum": [
+            "in progress",
+            "to do",
+            "done"
+          ]
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Milestone was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "categories": {
+          "description": "An array of IDs of Categories attached to the Milestone.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateCategoryParams"
+          }
+        },
+        "before_id": {
+          "description": "The ID of the Milestone we want to move this Milestone before.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "after_id": {
+          "description": "The ID of the Milestone we want to move this Milestone after.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateObjective": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The Objective's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A boolean indicating whether the Objective is archived or not",
+          "type": "boolean"
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Objective was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "maxLength": 256,
+          "minLength": 1,
+          "description": "The name of the Objective.",
+          "type": "string"
+        },
+        "state": {
+          "description": "The workflow state that the Objective is in.",
+          "type": "string",
+          "enum": [
+            "in progress",
+            "to do",
+            "done"
+          ]
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Objective was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "categories": {
+          "description": "An array of IDs of Categories attached to the Objective.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateCategoryParams"
+          }
+        },
+        "before_id": {
+          "description": "The ID of the Objective we want to move this Objective before.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "after_id": {
+          "description": "The ID of the Objective we want to move this Objective after.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateProject": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The Project's description.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "A true/false boolean indicating whether the Story is in archived state.",
+          "type": "boolean"
+        },
+        "days_to_thermometer": {
+          "description": "The number of days before the thermometer appears in the Story summary.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The color that represents the Project in the UI.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "name": {
+          "maxLength": 128,
+          "minLength": 1,
+          "description": "The Project's name.",
+          "type": "string"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members you want to add as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "show_thermometer": {
+          "description": "Configuration to enable or disable thermometers in the Story summary.",
+          "type": "boolean"
+        },
+        "team_id": {
+          "description": "The ID of the team the project belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "abbreviation": {
+          "description": "The Project abbreviation used in Story summaries. Should be kept to 3 characters at most.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateStories": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "archived": {
+          "description": "If the Stories should be archived or not.",
+          "type": "boolean"
+        },
+        "story_ids": {
+          "description": "The Ids of the Stories you wish to update.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "move_to": {
+          "description": "One of \"first\" or \"last\". This can be used to move the given story to the first or last position in the workflow state.",
+          "type": "string",
+          "enum": [
+            "last",
+            "first"
+          ]
+        },
+        "follower_ids_add": {
+          "description": "The UUIDs of the new followers to be added.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of External Links associated with this story.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "follower_ids_remove": {
+          "description": "The UUIDs of the followers to be removed.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "requested_by_id": {
+          "description": "The ID of the member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "custom_fields_remove": {
+          "description": "A map specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "labels_add": {
+          "description": "An array of labels to be added.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "group_id": {
+          "description": "The Id of the Group the Stories should belong to.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state to put the stories in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "before_id": {
+          "description": "The ID of the story that the stories are to be moved before.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "after_id": {
+          "description": "The ID of the story that the stories are to be moved below.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "owner_ids_remove": {
+          "description": "The UUIDs of the owners to be removed.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "custom_fields_add": {
+          "description": "A map specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "project_id": {
+          "description": "The ID of the Project the Stories should belong to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "labels_remove": {
+          "description": "An array of labels to be removed.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "owner_ids_add": {
+          "description": "The UUIDs of the new owners to be added.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "story_ids"
+      ]
+    },
+    "UpdateStory": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 100000,
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "archived": {
+          "description": "True if the story is archived, otherwise false.",
+          "type": "boolean"
+        },
+        "labels": {
+          "description": "An array of labels attached to the story.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "pull_request_ids": {
+          "description": "An array of IDs of Pull/Merge Requests attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string",
+          "enum": [
+            "feature",
+            "chore",
+            "bug"
+          ]
+        },
+        "custom_fields": {
+          "description": "A map specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "move_to": {
+          "description": "One of \"first\" or \"last\". This can be used to move the given story to the first or last position in the workflow state.",
+          "type": "string",
+          "enum": [
+            "last",
+            "first"
+          ]
+        },
+        "file_ids": {
+          "description": "An array of IDs of files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "completed_at_override": {
+          "description": "A manual override for the time/date the Story was completed.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "name": {
+          "maxLength": 512,
+          "minLength": 1,
+          "description": "The title of the story.",
+          "type": "string"
+        },
+        "epic_id": {
+          "description": "The ID of the epic the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of External Links associated with this story.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "branch_ids": {
+          "description": "An array of IDs of Branches attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "commit_ids": {
+          "description": "An array of IDs of Commits attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "sub_tasks": {
+          "description": "An array of story IDs to attach to this story as sub-tasks. This list represents the final state of the parent's sub-tasks - missing stories will be unlinked, new stories will be linked, and the input order reflects sub-task positions.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/LinkSubTaskParams"
+          }
+        },
+        "requested_by_id": {
+          "description": "The ID of the member that requested the story.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "started_at_override": {
+          "description": "A manual override for the time/date the Story was started.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "group_id": {
+          "description": "The ID of the group to associate with this story",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state to put the story in.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs of the followers of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "uniqueItems": true
+        },
+        "parent_story_id": {
+          "description": "The parent story id. If you want to unset this value set parent_story_id to null.",
+          "x-doc-skip": true,
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "before_id": {
+          "description": "The ID of the story we want to move this story before.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "estimate": {
+          "description": "The numeric point estimate of the story. Can also be null, which means unestimated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "after_id": {
+          "description": "The ID of the story we want to move this story after.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "linked_file_ids": {
+          "description": "An array of IDs of linked files attached to the story.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateStoryComment": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "text": {
+          "maxLength": 100000,
+          "description": "The updated comment text.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "text"
+      ]
+    },
+    "UpdateStoryContents": {
+      "description": "Updated attributes for the template to populate.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of the story.",
+          "type": "string"
+        },
+        "labels": {
+          "description": "An array of labels to be populated by the template.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateLabelParams"
+          }
+        },
+        "story_type": {
+          "description": "The type of story (feature, bug, chore).",
+          "type": "string"
+        },
+        "custom_fields": {
+          "description": "An array of maps specifying a CustomField ID and CustomFieldEnumValue ID that represents an assertion of some value for a CustomField.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CustomFieldValueParams"
+          }
+        },
+        "file_ids": {
+          "description": "An array of the attached file IDs to be populated.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "name": {
+          "description": "The name of the story.",
+          "type": "string"
+        },
+        "epic_id": {
+          "description": "The ID of the epic the to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "external_links": {
+          "description": "An array of external links to be populated.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "sub_tasks": {
+          "description": "An array of maps specifying the sub-tasks to create and link to the story.\nField only applicable when Sub-task feature is enabled.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CreateSubTaskParams"
+          }
+        },
+        "iteration_id": {
+          "description": "The ID of the iteration the to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "tasks": {
+          "description": "An array of tasks to be populated by the template.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/BaseTaskParams"
+          }
+        },
+        "group_id": {
+          "description": "The ID of the group to be populated.",
+          "type": "string",
+          "format": "uuid",
+          "x-nullable": true
+        },
+        "workflow_state_id": {
+          "description": "The ID of the workflow state to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "follower_ids": {
+          "description": "An array of UUIDs for any Members listed as Followers.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "estimate": {
+          "description": "The numeric point estimate to be populated.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "project_id": {
+          "description": "The ID of the project the story belongs to.",
+          "type": "integer",
+          "format": "int64",
+          "x-nullable": true
+        },
+        "linked_file_ids": {
+          "description": "An array of the linked file IDs to be populated.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "uniqueItems": true
+        },
+        "deadline": {
+          "description": "The due date of the story.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateStoryLink": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "verb": {
+          "description": "The type of link.",
+          "type": "string",
+          "enum": [
+            "blocks",
+            "duplicates",
+            "relates to"
+          ]
+        },
+        "subject_id": {
+          "description": "The ID of the subject Story.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "object_id": {
+          "description": "The ID of the object Story.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UpdateTask": {
+      "x-doc-skip": true,
+      "type": "object",
+      "properties": {
+        "description": {
+          "maxLength": 2048,
+          "minLength": 1,
+          "description": "The Task's description.",
+          "type": "string"
+        },
+        "owner_ids": {
+          "description": "An array of UUIDs of the owners of this story.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "complete": {
+          "description": "A true/false boolean indicating whether the task is complete.",
+          "type": "boolean"
+        },
+        "before_id": {
+          "description": "Move task before this task ID.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "after_id": {
+          "description": "Move task after this task ID.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false
+    },
+    "UploadedFile": {
+      "description": "An UploadedFile is any document uploaded to your Shortcut Workspace. Files attached from a third-party service are different: see the Linked Files endpoint.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of the file.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "story_ids": {
+          "description": "The unique IDs of the Stories associated with this file.",
+          "type": "array",
+          "items": {
+            "type": "integer",
+            "format": "int64"
+          }
+        },
+        "mention_ids": {
+          "description": "`Deprecated:` use `member_mention_ids`.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "member_mention_ids": {
+          "description": "The unique IDs of the Members who are mentioned in the file description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "name": {
+          "description": "The optional User-specified name of the file.",
+          "type": "string"
+        },
+        "thumbnail_url": {
+          "description": "The url where the thumbnail of the file can be found in Shortcut.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "size": {
+          "description": "The size of the file.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "uploader_id": {
+          "description": "The unique ID of the Member who uploaded the file.",
+          "type": "string",
+          "format": "uuid"
+        },
+        "content_type": {
+          "description": "Free form string corresponding to a text or image file.",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The time/date that the file was updated.",
+          "type": "string",
+          "format": "date-time",
+          "x-nullable": true
+        },
+        "filename": {
+          "description": "The name assigned to the file in Shortcut upon upload.",
+          "type": "string"
+        },
+        "group_mention_ids": {
+          "description": "The unique IDs of the Groups who are mentioned in the file description.",
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "uuid"
+          }
+        },
+        "external_id": {
+          "description": "This field can be set to another unique ID. In the case that the File has been imported from another tool, the ID in the other tool can be indicated here.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "id": {
+          "description": "The unique ID for the file.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "url": {
+          "description": "The URL for the file.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "created_at": {
+          "description": "The time/date that the file was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "story_ids",
+        "mention_ids",
+        "member_mention_ids",
+        "name",
+        "thumbnail_url",
+        "size",
+        "uploader_id",
+        "content_type",
+        "updated_at",
+        "filename",
+        "group_mention_ids",
+        "external_id",
+        "id",
+        "url",
+        "created_at"
+      ]
+    },
+    "Workflow": {
+      "description": "Workflow is the array of defined Workflow States. Workflow can be queried using the API but must be updated in the Shortcut UI. ",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "A description of the workflow.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "project_ids": {
+          "description": "An array of IDs of projects within the Workflow.",
+          "type": "array",
+          "items": {
+            "type": "number",
+            "format": "double"
+          }
+        },
+        "states": {
+          "description": "A map of the states in this Workflow.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/WorkflowState"
+          }
+        },
+        "name": {
+          "description": "The name of the workflow.",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "The date the Workflow was updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "auto_assign_owner": {
+          "description": "Indicates if an owner is automatically assigned when an unowned story is started.",
+          "type": "boolean"
+        },
+        "id": {
+          "description": "The unique ID of the Workflow.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "team_id": {
+          "description": "The ID of the team the workflow belongs to.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The date the Workflow was created.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "default_state_id": {
+          "description": "The unique ID of the default state that new Stories are entered into.",
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "project_ids",
+        "states",
+        "name",
+        "updated_at",
+        "auto_assign_owner",
+        "id",
+        "team_id",
+        "created_at",
+        "default_state_id"
+      ]
+    },
+    "WorkflowState": {
+      "description": "Workflow State is any of the at least 3 columns. Workflow States correspond to one of 3 types: Unstarted, Started, or Done.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "description": "The description of what sort of Stories belong in that Workflow state.",
+          "type": "string"
+        },
+        "entity_type": {
+          "description": "A string description of this resource.",
+          "type": "string"
+        },
+        "color": {
+          "pattern": "^#[a-fA-F0-9]{6}$",
+          "format": "css-color",
+          "description": "The hex color for this Workflow State.",
+          "minLength": 1,
+          "type": "string"
+        },
+        "verb": {
+          "description": "The verb that triggers a move to that Workflow State when making VCS commits.",
+          "type": "string",
+          "x-nullable": true
+        },
+        "name": {
+          "description": "The Workflow State's name.",
+          "type": "string"
+        },
+        "global_id": {
+          "x-doc-skip": true,
+          "type": "string"
+        },
+        "num_stories": {
+          "description": "The number of Stories currently in that Workflow State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "type": {
+          "description": "The type of Workflow State (Unstarted, Started, or Finished)",
+          "type": "string"
+        },
+        "updated_at": {
+          "description": "When the Workflow State was last updated.",
+          "type": "string",
+          "format": "date-time"
+        },
+        "id": {
+          "description": "The unique ID of the Workflow State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "num_story_templates": {
+          "description": "The number of Story Templates associated with that Workflow State.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "position": {
+          "description": "The position that the Workflow State is in, starting with 0 at the left.",
+          "type": "integer",
+          "format": "int64"
+        },
+        "created_at": {
+          "description": "The time/date the Workflow State was created.",
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "description",
+        "entity_type",
+        "verb",
+        "name",
+        "global_id",
+        "num_stories",
+        "type",
+        "updated_at",
+        "id",
+        "num_story_templates",
+        "position",
+        "created_at"
+      ]
+    }
+  },
+  "security": [
+    {
+      "api_token": []
+    }
+  ],
+  "produces": [
+    "application/json"
+  ],
+  "paths": {
+    "/api/v3/categories": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Category"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listCategories",
+        "description": "List Categories returns a list of all Categories and their attributes.",
+        "summary": "List Categories"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateCategory",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateCategory"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Category"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createCategory",
+        "description": "Create Category allows you to create a new Category in Shortcut.",
+        "summary": "Create Category"
+      }
+    },
+    "/api/v3/categories/{category-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "category-public-id",
+            "description": "The unique ID of the Category.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Category"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getCategory",
+        "description": "Get Category returns information about the selected Category.",
+        "summary": "Get Category"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "category-public-id",
+            "description": "The unique ID of the Category you wish to update.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateCategory",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateCategory"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Category"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateCategory",
+        "description": "Update Category allows you to replace a Category name with another name. If you try to name a Category something that already exists, you will receive a 422 response.",
+        "summary": "Update Category"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "category-public-id",
+            "description": "The unique ID of the Category.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteCategory",
+        "description": "Delete Category can be used to delete any Category.",
+        "summary": "Delete Category"
+      }
+    },
+    "/api/v3/categories/{category-public-id}/milestones": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "category-public-id",
+            "description": "The unique ID of the Category.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Milestone"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listCategoryMilestones",
+        "description": "List Category Milestones returns a list of all Milestones with the Category.",
+        "summary": "List Category Milestones"
+      }
+    },
+    "/api/v3/categories/{category-public-id}/objectives": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "category-public-id",
+            "description": "The unique ID of the Category.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Milestone"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listCategoryObjectives",
+        "description": "Returns a list of all Objectives with the Category.",
+        "summary": "List Category Objectives"
+      }
+    },
+    "/api/v3/custom-fields": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/CustomField"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listCustomFields",
+        "summary": "List Custom Fields"
+      }
+    },
+    "/api/v3/custom-fields/{custom-field-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "custom-field-public-id",
+            "description": "The unique ID of the CustomField.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/CustomField"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getCustomField",
+        "summary": "Get Custom Field"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "custom-field-public-id",
+            "description": "The unique ID of the CustomField.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "UpdateCustomField",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateCustomField"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/CustomField"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "409": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DataConflictError"
+            }
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateCustomField",
+        "description": "Update Custom Field can be used to update the definition of a Custom Field. The order of items in the 'values' collection is interpreted to be their ascending sort order.To delete an existing enum value, simply omit it from the 'values' collection. New enum values may be created inline by including an object in the 'values' collection having a 'value' entry with no 'id' (eg. {'value': 'myNewValue', 'color_key': 'green'}).",
+        "summary": "Update Custom Field"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "custom-field-public-id",
+            "description": "The unique ID of the CustomField.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteCustomField",
+        "summary": "Delete Custom Field"
+      }
+    },
+    "/api/v3/documents": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/DocSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listDocs",
+        "description": "List Docs returns a list of Doc that the current user can read.",
+        "summary": "List Docs"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateDoc",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateDoc"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/DocSlim"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createDoc",
+        "description": "Creates a new Doc. Supports markdown or HTML input via content_format parameter.",
+        "summary": "Create Doc"
+      }
+    },
+    "/api/v3/documents/{doc-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "doc-public-id",
+            "description": "The Doc's public ID",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "query",
+            "name": "content_format",
+            "description": "Format of the content to return. Defaults to 'markdown'. If 'html', includes HTML content in response.",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "markdown",
+              "html"
+            ]
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Doc"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getDoc",
+        "description": "Get a Doc by its public ID with content. Use content_format=html query parameter to include HTML content.",
+        "summary": "Get Doc"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "doc-public-id",
+            "description": "The Doc's public ID",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "UpdateDoc",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateDoc"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Doc"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "409": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/ConflictError"
+            }
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateDoc",
+        "description": "Updates an existing Doc's title and/or content. Supports markdown or HTML input via content_format parameter. When a document is updated: (1) If users are connected, SSE events notify them to refresh their view. (2) If no users are connected, CKEditor cache is flushed to ensure fresh content.",
+        "summary": "Update Doc"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "doc-public-id",
+            "description": "The Doc's public ID",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "GetDoc",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/GetDoc"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteDoc",
+        "description": "Permanently deletes a Doc and its associated data. Requires admin access to the doc. When a document is deleted, connected clients will be notified via SSE events.",
+        "summary": "Delete Doc"
+      }
+    },
+    "/api/v3/documents/{doc-public-id}/epics": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "doc-public-id",
+            "description": "The public ID of the Document.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/EpicSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listDocumentEpics",
+        "description": "Get a list of all Epics related to this Document.",
+        "summary": "List Document Epics"
+      }
+    },
+    "/api/v3/documents/{doc-public-id}/epics/{epic-public-id}": {
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "doc-public-id",
+            "description": "The public ID of the Document.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The public ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "**Either:** (1) Unprocessable **or** (2)  ",
+            "schema": {
+              "$ref": "#/definitions/UnprocessableError"
+            }
+          }
+        },
+        "operationId": "linkDocumentToEpic",
+        "description": "Create a relationship between a Document and an Epic.",
+        "summary": "Link Document to Epic"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "doc-public-id",
+            "description": "The public ID of the Document.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The public ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "unlinkDocumentFromEpic",
+        "description": "Remove a relationship between a Document and an Epic.",
+        "summary": "Unlink Document from Epic"
+      }
+    },
+    "/api/v3/entity-templates": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/EntityTemplate"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEntityTemplates",
+        "description": "List all the entity templates for the Workspace.",
+        "summary": "List Entity Templates"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateEntityTemplate",
+            "description": "Request parameters for creating an entirely new entity template.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateEntityTemplate"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/EntityTemplate"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createEntityTemplate",
+        "description": "Create a new entity template for the Workspace.",
+        "summary": "Create Entity Template"
+      }
+    },
+    "/api/v3/entity-templates/disable": {
+      "put": {
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "disableStoryTemplates",
+        "description": "Disables the Story Template feature for the Workspace.",
+        "summary": "Disable Story Templates"
+      }
+    },
+    "/api/v3/entity-templates/enable": {
+      "put": {
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "enableStoryTemplates",
+        "description": "Enables the Story Template feature for the Workspace.",
+        "summary": "Enable Story Templates"
+      }
+    },
+    "/api/v3/entity-templates/{entity-template-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "entity-template-public-id",
+            "description": "The unique ID of the entity template.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/EntityTemplate"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getEntityTemplate",
+        "description": "Get Entity Template returns information about a given entity template.",
+        "summary": "Get Entity Template"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "entity-template-public-id",
+            "description": "The unique ID of the template to be updated.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "UpdateEntityTemplate",
+            "description": "Request parameters for changing either a template's name or any of\n  the attributes it is designed to pre-populate.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateEntityTemplate"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/EntityTemplate"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateEntityTemplate",
+        "description": "Update an entity template's name or its contents.",
+        "summary": "Update Entity Template"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "entity-template-public-id",
+            "description": "The unique ID of the entity template.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteEntityTemplate",
+        "summary": "Delete Entity Template"
+      }
+    },
+    "/api/v3/epic-workflow": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/EpicWorkflow"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getEpicWorkflow",
+        "description": "Returns the Epic Workflow for the Workspace.",
+        "summary": "Get Epic Workflow"
+      }
+    },
+    "/api/v3/epics": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "includes_description",
+            "description": "A true/false boolean indicating whether to return Epics with their descriptions.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/EpicSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEpics",
+        "description": "List Epics returns a list of all Epics and their attributes.",
+        "summary": "List Epics"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateEpic",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateEpic"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Epic"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createEpic",
+        "description": "Create Epic allows you to create a new Epic in Shortcut.",
+        "summary": "Create Epic"
+      }
+    },
+    "/api/v3/epics/paginated": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "includes_description",
+            "description": "A true/false boolean indicating whether to return Epics with their descriptions.",
+            "required": false,
+            "type": "boolean"
+          },
+          {
+            "in": "query",
+            "name": "page",
+            "description": "The page number to return, starting with 1. Defaults to 1.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of Epics to return per page. Minimum 1, maximum 250, default 10.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/EpicPaginatedResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEpicsPaginated",
+        "description": "List Epics with pagination returns a paginated list of Epics and their attributes.",
+        "summary": "List Epics Paginated"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Epic"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getEpic",
+        "description": "Get Epic returns information about the selected Epic.",
+        "summary": "Get Epic"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateEpic",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateEpic"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Epic"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateEpic",
+        "description": "Update Epic can be used to update numerous fields in the Epic. The only required parameter is Epic ID, which can be found in the Shortcut UI.",
+        "summary": "Update Epic"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteEpic",
+        "description": "Delete Epic can be used to delete the Epic. The only required parameter is Epic ID.",
+        "summary": "Delete Epic"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}/comments": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/ThreadedComment"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEpicComments",
+        "description": "Get a list of all Comments on an Epic.",
+        "summary": "List Epic Comments"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The ID of the associated Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateEpicComment",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateEpicComment"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/ThreadedComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createEpicComment",
+        "description": "This endpoint allows you to create a threaded Comment on an Epic.",
+        "summary": "Create Epic Comment"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}/comments/{comment-public-id}": {
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The ID of the associated Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the parent Epic Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateCommentComment",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateCommentComment"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/ThreadedComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createEpicCommentComment",
+        "description": "This endpoint allows you to create a nested Comment reply to an existing Epic Comment.",
+        "summary": "Create Epic Comment Comment"
+      },
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The ID of the associated Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/ThreadedComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getEpicComment",
+        "description": "This endpoint returns information about the selected Epic Comment.",
+        "summary": "Get Epic Comment"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The ID of the associated Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateComment",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateComment"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/ThreadedComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateEpicComment",
+        "description": "This endpoint allows you to update a threaded Comment on an Epic.",
+        "summary": "Update Epic Comment"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The ID of the associated Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteEpicComment",
+        "description": "This endpoint allows you to delete a Comment from an Epic.",
+        "summary": "Delete Epic Comment"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}/documents": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/DocSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEpicDocuments",
+        "description": "Get a list of all Documents related to this Epic.",
+        "summary": "List Epic Documents"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}/health": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Health"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getEpicHealth",
+        "description": "Get the current health for the specified Epic.",
+        "summary": "Get Epic Health"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateEpicHealth",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateEpicHealth"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Health"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createEpicHealth",
+        "description": "Create a new health status for the specified Epic.",
+        "summary": "Create Epic Health"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}/health-history": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Health"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEpicHealths",
+        "description": "List the history of health statuses for the specified Epic, most recent first.",
+        "summary": "List Epic Healths"
+      }
+    },
+    "/api/v3/epics/{epic-public-id}/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "epic-public-id",
+            "description": "The unique ID of the Epic.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "includes_description",
+            "description": "A true/false boolean indicating whether to return Stories with their descriptions.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listEpicStories",
+        "description": "Get a list of all Stories in an Epic.",
+        "summary": "List Epic Stories"
+      }
+    },
+    "/api/v3/external-link/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "external_link",
+            "description": "The external link associated with one or more stories.",
+            "required": true,
+            "pattern": "^https?://.+$",
+            "maxLength": 2048,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getExternalLinkStories",
+        "description": "Get Stories which have a given External Link associated with them.",
+        "summary": "Get External Link Stories"
+      }
+    },
+    "/api/v3/files": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UploadedFile"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listFiles",
+        "description": "List Files returns a list of all UploadedFiles in the workspace.",
+        "summary": "List Files"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "formData",
+            "name": "story_id",
+            "description": "The story ID that these files will be associated with.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "formData",
+            "name": "file0",
+            "description": "A file upload. At least one is required.",
+            "required": true,
+            "type": "file"
+          },
+          {
+            "in": "formData",
+            "name": "file1",
+            "description": "Optional additional files.",
+            "required": false,
+            "type": "file"
+          },
+          {
+            "in": "formData",
+            "name": "file2",
+            "description": "Optional additional files.",
+            "required": false,
+            "type": "file"
+          },
+          {
+            "in": "formData",
+            "name": "file3",
+            "description": "Optional additional files.",
+            "required": false,
+            "type": "file"
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/UploadedFile"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "uploadFiles",
+        "consumes": [
+          "multipart/form-data"
+        ],
+        "description": "Upload Files uploads one or many files and optionally associates them with a story.\n   Use the multipart/form-data content-type to upload.\n   Each `file` key should contain a separate file.\n   Each UploadedFile's name comes from the Content-Disposition header \"filename\" directive for that field.",
+        "summary": "Upload Files"
+      }
+    },
+    "/api/v3/files/{file-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "file-public-id",
+            "description": "The File’s unique ID.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/UploadedFile"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getFile",
+        "description": "Get File returns information about the selected UploadedFile.",
+        "summary": "Get File"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "file-public-id",
+            "description": "The unique ID assigned to the file in Shortcut.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateFile",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateFile"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/UploadedFile"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateFile",
+        "description": "Update File updates the properties of an UploadedFile (but not its content).",
+        "summary": "Update File"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "file-public-id",
+            "description": "The File’s unique ID.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteFile",
+        "description": "Delete File deletes a previously uploaded file.",
+        "summary": "Delete File"
+      }
+    },
+    "/api/v3/groups": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "archived",
+            "description": "Filter groups by their archived state. If true, returns only archived groups. If false, returns only unarchived groups. If not provided, returns all groups",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Group"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listGroups",
+        "description": "A group in our API maps to a \"Team\" within the Shortcut Product. A Team is a collection of Users that can be associated to Stories, Epics, and Iterations within Shortcut.",
+        "summary": "List Groups"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateGroup",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateGroup"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Group"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/UnusableEntitlementError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createGroup",
+        "summary": "Create Group"
+      }
+    },
+    "/api/v3/groups/{group-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "group-public-id",
+            "description": "The unique ID of the Group.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Group"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getGroup",
+        "summary": "Get Group"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "group-public-id",
+            "description": "The unique ID of the Group.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "UpdateGroup",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateGroup"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Group"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/UnusableEntitlementError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateGroup",
+        "summary": "Update Group"
+      }
+    },
+    "/api/v3/groups/{group-public-id}/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "group-public-id",
+            "description": "The unique ID of the Group.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "query",
+            "name": "limit",
+            "description": "The maximum number of results to return. (Defaults to 1000, max 1000)",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "offset",
+            "description": "The offset at which to begin returning results. (Defaults to 0)",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listGroupStories",
+        "description": "List the Stories assigned to the Group. (By default, limited to 1,000).",
+        "summary": "List Group Stories"
+      }
+    },
+    "/api/v3/health/{health-public-id}": {
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "health-public-id",
+            "description": "The unique ID of the Health record.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "UpdateHealth",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateHealth"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Health"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateHealth",
+        "description": "Update an existing health status by its ID.",
+        "summary": "Update Health"
+      }
+    },
+    "/api/v3/integrations/webhook": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateGenericIntegration",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateGenericIntegration"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createGenericIntegration",
+        "summary": "Create Generic Integration"
+      }
+    },
+    "/api/v3/integrations/webhook/{integration-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "integration-public-id",
+            "description": "",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getGenericIntegration",
+        "summary": "Get Generic Integration"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "integration-public-id",
+            "description": "",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteGenericIntegration",
+        "summary": "Delete Generic Integration"
+      }
+    },
+    "/api/v3/iterations": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/IterationSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listIterations",
+        "summary": "List Iterations"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateIteration",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateIteration"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Iteration"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createIteration",
+        "summary": "Create Iteration"
+      }
+    },
+    "/api/v3/iterations/disable": {
+      "put": {
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "disableIterations",
+        "description": "Disables Iterations for the current workspace",
+        "summary": "Disable Iterations"
+      }
+    },
+    "/api/v3/iterations/enable": {
+      "put": {
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "enableIterations",
+        "description": "Enables Iterations for the current workspace",
+        "summary": "Enable Iterations"
+      }
+    },
+    "/api/v3/iterations/{iteration-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "iteration-public-id",
+            "description": "The unique ID of the Iteration.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Iteration"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getIteration",
+        "summary": "Get Iteration"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "iteration-public-id",
+            "description": "The unique ID of the Iteration.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateIteration",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateIteration"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Iteration"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateIteration",
+        "summary": "Update Iteration"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "iteration-public-id",
+            "description": "The unique ID of the Iteration.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteIteration",
+        "summary": "Delete Iteration"
+      }
+    },
+    "/api/v3/iterations/{iteration-public-id}/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "iteration-public-id",
+            "description": "The unique ID of the Iteration.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "includes_description",
+            "description": "A true/false boolean indicating whether to return Stories with their descriptions.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listIterationStories",
+        "description": "Get a list of all Stories in an Iteration.",
+        "summary": "List Iteration Stories"
+      }
+    },
+    "/api/v3/key-results/{key-result-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "key-result-public-id",
+            "description": "The ID of the Key Result.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/KeyResult"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getKeyResult",
+        "description": "Get Key Result returns information about a chosen Key Result.",
+        "summary": "Get Key Result"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "key-result-public-id",
+            "description": "The ID of the Key Result.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "body",
+            "name": "UpdateKeyResult",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateKeyResult"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/KeyResult"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateKeyResult",
+        "description": "Update Key Result allows updating a Key Result's name or initial, observed, or target values.",
+        "summary": "Update Key Result"
+      }
+    },
+    "/api/v3/labels": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "slim",
+            "description": "A true/false boolean indicating if the slim versions of the Label should be returned.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Label"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listLabels",
+        "description": "List Labels returns a list of all Labels and their attributes.",
+        "summary": "List Labels"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateLabelParams",
+            "description": "Request parameters for creating a Label on a Shortcut Story.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateLabelParams"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Label"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createLabel",
+        "description": "Create Label allows you to create a new Label in Shortcut.",
+        "summary": "Create Label"
+      }
+    },
+    "/api/v3/labels/{label-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "label-public-id",
+            "description": "The unique ID of the Label.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Label"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getLabel",
+        "description": "Get Label returns information about the selected Label.",
+        "summary": "Get Label"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "label-public-id",
+            "description": "The unique ID of the Label you wish to update.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateLabel",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateLabel"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Label"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateLabel",
+        "description": "Update Label allows you to replace a Label name with another name. If you try to name a Label something that already exists, you will receive a 422 response.",
+        "summary": "Update Label"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "label-public-id",
+            "description": "The unique ID of the Label.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteLabel",
+        "description": "Delete Label can be used to delete any Label.",
+        "summary": "Delete Label"
+      }
+    },
+    "/api/v3/labels/{label-public-id}/epics": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "label-public-id",
+            "description": "The unique ID of the Label.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/EpicSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listLabelEpics",
+        "description": "List all of the Epics with the Label.",
+        "summary": "List Label Epics"
+      }
+    },
+    "/api/v3/labels/{label-public-id}/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "label-public-id",
+            "description": "The unique ID of the Label.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "includes_description",
+            "description": "A true/false boolean indicating whether to return Stories with their descriptions.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listLabelStories",
+        "description": "List all of the Stories with the Label.",
+        "summary": "List Label Stories"
+      }
+    },
+    "/api/v3/linked-files": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/LinkedFile"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listLinkedFiles",
+        "description": "List Linked Files returns a list of all Linked-Files and their attributes.",
+        "summary": "List Linked Files"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateLinkedFile",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateLinkedFile"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/LinkedFile"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createLinkedFile",
+        "description": "Create Linked File allows you to create a new Linked File in Shortcut.",
+        "summary": "Create Linked File"
+      }
+    },
+    "/api/v3/linked-files/{linked-file-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "linked-file-public-id",
+            "description": "The unique identifier of the linked file.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/LinkedFile"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getLinkedFile",
+        "description": "Get File returns information about the selected Linked File.",
+        "summary": "Get Linked File"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "linked-file-public-id",
+            "description": "The unique identifier of the linked file.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateLinkedFile",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateLinkedFile"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/LinkedFile"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateLinkedFile",
+        "description": "Updated Linked File allows you to update properties of a previously attached Linked-File.",
+        "summary": "Update Linked File"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "linked-file-public-id",
+            "description": "The unique identifier of the linked file.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteLinkedFile",
+        "description": "Delete Linked File can be used to delete any previously attached Linked-File.",
+        "summary": "Delete Linked File"
+      }
+    },
+    "/api/v3/member": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/MemberInfo"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getCurrentMemberInfo",
+        "description": "Returns information about the authenticated member.",
+        "summary": "Get Current Member Info"
+      }
+    },
+    "/api/v3/members": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "org-public-id",
+            "description": "The unique ID of the Organization to limit the list to.",
+            "required": false,
+            "x-doc-skip": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "query",
+            "name": "disabled",
+            "description": "Filter members by their disabled state. If true, return only disabled members. If false, return only enabled members.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Member"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listMembers",
+        "description": "Returns information about members of the Workspace.",
+        "summary": "List Members"
+      }
+    },
+    "/api/v3/members/{member-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "member-public-id",
+            "description": "The Member's unique ID.",
+            "required": true,
+            "type": "string",
+            "format": "uuid"
+          },
+          {
+            "in": "query",
+            "name": "org-public-id",
+            "description": "The unique ID of the Organization to limit the lookup to.",
+            "required": false,
+            "x-doc-skip": true,
+            "type": "string",
+            "format": "uuid"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Member"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getMember",
+        "description": "Returns information about a Member.",
+        "summary": "Get Member"
+      }
+    },
+    "/api/v3/milestones": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Milestone"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listMilestones",
+        "deprecated": true,
+        "description": "(Deprecated: Use 'List Objectives') List Milestones returns a list of all Milestones and their attributes.",
+        "summary": "List Milestones"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateMilestone",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateMilestone"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Milestone"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/UnusableEntitlementError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createMilestone",
+        "deprecated": true,
+        "description": "(Deprecated: Use 'Create Objective') Create Milestone allows you to create a new Milestone in Shortcut.",
+        "summary": "Create Milestone"
+      }
+    },
+    "/api/v3/milestones/{milestone-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "milestone-public-id",
+            "description": "The ID of the Milestone.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Milestone"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getMilestone",
+        "deprecated": true,
+        "description": "(Deprecated: Use 'Get Objective') Get Milestone returns information about a chosen Milestone.",
+        "summary": "Get Milestone"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "milestone-public-id",
+            "description": "The ID of the Milestone.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateMilestone",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateMilestone"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Milestone"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateMilestone",
+        "deprecated": true,
+        "description": "(Deprecated: Use 'Update Objective') Update Milestone can be used to update Milestone properties.",
+        "summary": "Update Milestone"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "milestone-public-id",
+            "description": "The ID of the Milestone.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteMilestone",
+        "deprecated": true,
+        "description": "(Deprecated: Use 'Delete Objective') Delete Milestone can be used to delete any Milestone.",
+        "summary": "Delete Milestone"
+      }
+    },
+    "/api/v3/milestones/{milestone-public-id}/epics": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "milestone-public-id",
+            "description": "The ID of the Milestone.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/EpicSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listMilestoneEpics",
+        "deprecated": true,
+        "description": "(Deprecated: Use 'List Objective Epics') List all of the Epics within the Milestone.",
+        "summary": "List Milestone Epics"
+      }
+    },
+    "/api/v3/objectives": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Objective"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listObjectives",
+        "description": "List Objectives returns a list of all Objectives and their attributes.",
+        "summary": "List Objectives"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateObjective",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateObjective"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Objective"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/UnusableEntitlementError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createObjective",
+        "description": "Create Objective allows you to create a new Objective in Shortcut.",
+        "summary": "Create Objective"
+      }
+    },
+    "/api/v3/objectives/{objective-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Objective"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getObjective",
+        "description": "Get Objective returns information about a chosen Objective.",
+        "summary": "Get Objective"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateObjective",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateObjective"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Objective"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateObjective",
+        "description": "Update Objective can be used to update Objective properties.",
+        "summary": "Update Objective"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteObjective",
+        "description": "Delete Objective can be used to delete any Objective.",
+        "summary": "Delete Objective"
+      }
+    },
+    "/api/v3/objectives/{objective-public-id}/epics": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/EpicSlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listObjectiveEpics",
+        "description": "List all of the Epics within the Objective.",
+        "summary": "List Objective Epics"
+      }
+    },
+    "/api/v3/objectives/{objective-public-id}/health": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The unique ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Health"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getObjectiveHealth",
+        "description": "Get the current health for the specified Objective.",
+        "summary": "Get Objective Health"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The unique ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateObjectiveHealth",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateObjectiveHealth"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Health"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createObjectiveHealth",
+        "description": "Create a new health status for the specified Objective.",
+        "summary": "Create Objective Health"
+      }
+    },
+    "/api/v3/objectives/{objective-public-id}/health-history": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "objective-public-id",
+            "description": "The unique ID of the Objective.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Health"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listObjectiveHealths",
+        "description": "List the history of health statuses for the specified Objective, most recent first.",
+        "summary": "List Objective Healths"
+      }
+    },
+    "/api/v3/projects": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Project"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listProjects",
+        "description": "List Projects returns a list of all Projects and their attributes.",
+        "summary": "List Projects"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateProject",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateProject"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Project"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createProject",
+        "description": "Create Project is used to create a new Shortcut Project.",
+        "summary": "Create Project"
+      }
+    },
+    "/api/v3/projects/{project-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "project-public-id",
+            "description": "The unique ID of the Project.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Project"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getProject",
+        "description": "Get Project returns information about the selected Project.",
+        "summary": "Get Project"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "project-public-id",
+            "description": "The unique ID of the Project.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateProject",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateProject"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Project"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateProject",
+        "description": "Update Project can be used to change properties of a Project.",
+        "summary": "Update Project"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "project-public-id",
+            "description": "The unique ID of the Project.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteProject",
+        "description": "Delete Project can be used to delete a Project. Projects can only be deleted if all associated Stories are moved or deleted. In the case that the Project cannot be deleted, you will receive a 422 response.",
+        "summary": "Delete Project"
+      }
+    },
+    "/api/v3/projects/{project-public-id}/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "project-public-id",
+            "description": "The unique ID of the Project.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "includes_description",
+            "description": "A true/false boolean indicating whether to return Stories with their descriptions.",
+            "required": false,
+            "type": "boolean"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listStories",
+        "description": "List Stories returns a list of all Stories in a selected Project and their attributes.",
+        "summary": "List Stories"
+      }
+    },
+    "/api/v3/repositories": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Repository"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listRepositories",
+        "description": "List Repositories returns a list of all Repositories and their attributes.",
+        "summary": "List Repositories"
+      }
+    },
+    "/api/v3/repositories/{repo-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "repo-public-id",
+            "description": "The unique ID of the Repository.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Repository"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getRepository",
+        "description": "Get Repository returns information about the selected Repository.",
+        "summary": "Get Repository"
+      }
+    },
+    "/api/v3/search": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "query",
+            "description": "See our help center article on [search operators](https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators)",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "detail",
+            "description": "The amount of detail included in each result item.\n   \"full\" will include all descriptions and comments and more fields on\n   related items such as pull requests, branches and tasks.\n   \"slim\" omits larger fulltext fields such as descriptions and comments\n   and only references related items by id.\n   The default is \"full\".",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "full",
+              "slim"
+            ]
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "entity_types",
+            "description": "A collection of entity_types to search. Defaults to story and epic. Supports: epic, iteration, objective, story.",
+            "required": false,
+            "type": "array",
+            "items": {
+              "type": "string",
+              "enum": [
+                "story",
+                "milestone",
+                "epic",
+                "iteration",
+                "objective"
+              ]
+            },
+            "collectionFormat": "multi"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/SearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "search",
+        "description": "Search lets you search Epics and Stories based on desired parameters. Since ordering of the results can change over time (due to search ranking decay, new Epics and Stories being created), the `next` value from the previous response can be used as the path and query string for the next page to ensure stable ordering.",
+        "summary": "Search"
+      }
+    },
+    "/api/v3/search/documents": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "title",
+            "description": "Search text to match against document titles. Supports fuzzy matching. Required.",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "archived",
+            "description": "When true, find archived documents. When false, find non-archived documents.",
+            "required": false,
+            "type": "boolean"
+          },
+          {
+            "in": "query",
+            "name": "created_by_me",
+            "description": "When true, find documents created by the current user. When false, find documents NOT created by current user.",
+            "required": false,
+            "type": "boolean"
+          },
+          {
+            "in": "query",
+            "name": "followed_by_me",
+            "description": "When true, find documents that the current user is following. When false, find documents NOT followed.",
+            "required": false,
+            "type": "boolean"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/DocSearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "403": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/DisabledFeatureError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "searchDocuments",
+        "description": "Search Documents lets you search Documents based on desired parameters.\n                          Supports structured filters: title, archived, created_by_me, followed_by_me.",
+        "summary": "Search Documents"
+      }
+    },
+    "/api/v3/search/epics": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "query",
+            "description": "See our help center article on [search operators](https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators)",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "detail",
+            "description": "The amount of detail included in each result item.\n   \"full\" will include all descriptions and comments and more fields on\n   related items such as pull requests, branches and tasks.\n   \"slim\" omits larger fulltext fields such as descriptions and comments\n   and only references related items by id.\n   The default is \"full\".",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "full",
+              "slim"
+            ]
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "entity_types",
+            "description": "A collection of entity_types to search. Defaults to story and epic. Supports: epic, iteration, objective, story.",
+            "required": false,
+            "type": "array",
+            "items": {
+              "type": "string",
+              "enum": [
+                "story",
+                "milestone",
+                "epic",
+                "iteration",
+                "objective"
+              ]
+            },
+            "collectionFormat": "multi"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/EpicSearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "searchEpics",
+        "description": "Search Epics lets you search Epics based on desired parameters. Since ordering of stories can change over time (due to search ranking decay, new Epics being created), the `next` value from the previous response can be used as the path and query string for the next page to ensure stable ordering.",
+        "summary": "Search Epics"
+      }
+    },
+    "/api/v3/search/iterations": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "query",
+            "description": "See our help center article on [search operators](https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators)",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "detail",
+            "description": "The amount of detail included in each result item.\n   \"full\" will include all descriptions and comments and more fields on\n   related items such as pull requests, branches and tasks.\n   \"slim\" omits larger fulltext fields such as descriptions and comments\n   and only references related items by id.\n   The default is \"full\".",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "full",
+              "slim"
+            ]
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "entity_types",
+            "description": "A collection of entity_types to search. Defaults to story and epic. Supports: epic, iteration, objective, story.",
+            "required": false,
+            "type": "array",
+            "items": {
+              "type": "string",
+              "enum": [
+                "story",
+                "milestone",
+                "epic",
+                "iteration",
+                "objective"
+              ]
+            },
+            "collectionFormat": "multi"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/IterationSearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "searchIterations",
+        "description": "Search Iterations lets you search Iterations based on desired parameters. Since ordering of results can change over time (due to search ranking decay, new Iterations being created), the `next` value from the previous response can be used as the path and query string for the next page to ensure stable ordering.",
+        "summary": "Search Iterations"
+      }
+    },
+    "/api/v3/search/milestones": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "query",
+            "description": "See our help center article on [search operators](https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators)",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "detail",
+            "description": "The amount of detail included in each result item.\n   \"full\" will include all descriptions and comments and more fields on\n   related items such as pull requests, branches and tasks.\n   \"slim\" omits larger fulltext fields such as descriptions and comments\n   and only references related items by id.\n   The default is \"full\".",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "full",
+              "slim"
+            ]
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "entity_types",
+            "description": "A collection of entity_types to search. Defaults to story and epic. Supports: epic, iteration, objective, story.",
+            "required": false,
+            "type": "array",
+            "items": {
+              "type": "string",
+              "enum": [
+                "story",
+                "milestone",
+                "epic",
+                "iteration",
+                "objective"
+              ]
+            },
+            "collectionFormat": "multi"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/ObjectiveSearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "searchMilestones",
+        "description": "Search Milestones lets you search Milestones based on desired parameters. Since ordering of results can change over time (due to search ranking decay, new Milestones being created), the `next` value from the previous response can be used as the path and query string for the next page to ensure stable ordering.",
+        "summary": "Search Milestones"
+      }
+    },
+    "/api/v3/search/objectives": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "query",
+            "description": "See our help center article on [search operators](https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators)",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "detail",
+            "description": "The amount of detail included in each result item.\n   \"full\" will include all descriptions and comments and more fields on\n   related items such as pull requests, branches and tasks.\n   \"slim\" omits larger fulltext fields such as descriptions and comments\n   and only references related items by id.\n   The default is \"full\".",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "full",
+              "slim"
+            ]
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "entity_types",
+            "description": "A collection of entity_types to search. Defaults to story and epic. Supports: epic, iteration, objective, story.",
+            "required": false,
+            "type": "array",
+            "items": {
+              "type": "string",
+              "enum": [
+                "story",
+                "milestone",
+                "epic",
+                "iteration",
+                "objective"
+              ]
+            },
+            "collectionFormat": "multi"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/ObjectiveSearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "searchObjectives",
+        "description": "Search Objectives lets you search Objectives based on desired parameters. Since ordering of results can change over time (due to search ranking decay, new Objectives being created), the `next` value from the previous response can be used as the path and query string for the next page to ensure stable ordering.",
+        "summary": "Search Objectives"
+      }
+    },
+    "/api/v3/search/stories": {
+      "get": {
+        "parameters": [
+          {
+            "in": "query",
+            "name": "query",
+            "description": "See our help center article on [search operators](https://help.shortcut.com/hc/en-us/articles/360000046646-Search-Operators)",
+            "required": true,
+            "minLength": 1,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "page_size",
+            "description": "The number of search results to include in a page. Minimum of 1 and maximum of 250.",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "query",
+            "name": "detail",
+            "description": "The amount of detail included in each result item.\n   \"full\" will include all descriptions and comments and more fields on\n   related items such as pull requests, branches and tasks.\n   \"slim\" omits larger fulltext fields such as descriptions and comments\n   and only references related items by id.\n   The default is \"full\".",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "full",
+              "slim"
+            ]
+          },
+          {
+            "in": "query",
+            "name": "next",
+            "description": "The next page token.",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "in": "query",
+            "name": "entity_types",
+            "description": "A collection of entity_types to search. Defaults to story and epic. Supports: epic, iteration, objective, story.",
+            "required": false,
+            "type": "array",
+            "items": {
+              "type": "string",
+              "enum": [
+                "story",
+                "milestone",
+                "epic",
+                "iteration",
+                "objective"
+              ]
+            },
+            "collectionFormat": "multi"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/StorySearchResults"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "**Either:** (1) Schema mismatch **or** (2) Maximum of 1000 search results exceeded ",
+            "schema": {
+              "$ref": "#/definitions/MaxSearchResultsExceededError"
+            }
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "searchStories",
+        "description": "Search Stories lets you search Stories based on desired parameters. Since ordering of stories can change over time (due to search ranking decay, new stories being created), the `next` value from the previous response can be used as the path and query string for the next page to ensure stable ordering.",
+        "summary": "Search Stories"
+      }
+    },
+    "/api/v3/stories": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateStoryParams",
+            "description": "Request parameters for creating a story.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateStoryParams"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Story"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createStory",
+        "description": "Create Story is used to add a new story to your Shortcut Workspace.\n    This endpoint requires that either **workflow_state_id** or **project_id** be provided, but will reject the request if both or neither are specified. The workflow_state_id has been marked as required and is the recommended field to specify because we are in the process of sunsetting Projects in Shortcut.",
+        "summary": "Create Story"
+      }
+    },
+    "/api/v3/stories/bulk": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateStories",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateStories"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createMultipleStories",
+        "description": "Create Multiple Stories allows you to create multiple stories in a single request using the same syntax as [Create Story](https://developer.shortcut.com/api/rest/v3#create-story).",
+        "summary": "Create Multiple Stories"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "UpdateStories",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateStories"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateMultipleStories",
+        "description": "Update Multiple Stories allows you to make changes to numerous stories at once.",
+        "summary": "Update Multiple Stories"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "DeleteStories",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/DeleteStories"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteMultipleStories",
+        "description": "Delete Multiple Stories allows you to delete multiple archived stories at once.",
+        "summary": "Delete Multiple Stories"
+      }
+    },
+    "/api/v3/stories/from-template": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateStoryFromTemplateParams",
+            "description": "Request parameters for creating a story from a story template. These parameters are merged with the values derived from the template.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateStoryFromTemplateParams"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Story"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createStoryFromTemplate",
+        "description": "Create Story From Template is used to add a new story derived from a template to your Shortcut Workspace.",
+        "summary": "Create Story From Template"
+      }
+    },
+    "/api/v3/stories/search": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "SearchStories",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/SearchStories"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "queryStories",
+        "description": "Search Stories lets you search Stories based on desired parameters.",
+        "summary": "Query Stories"
+      }
+    },
+    "/api/v3/stories/{story-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Story"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getStory",
+        "description": "Get Story returns information about a chosen Story.",
+        "summary": "Get Story"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The unique identifier of this story.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateStory",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateStory"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Story"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateStory",
+        "description": "Update Story can be used to update Story properties.",
+        "summary": "Update Story"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteStory",
+        "description": "Delete Story can be used to delete any Story.",
+        "summary": "Delete Story"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/comments": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StoryComment"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listStoryComment",
+        "description": "Lists Comments associated with a Story",
+        "summary": "List Story Comment"
+      },
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateStoryComment",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateStoryComment"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/StoryComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createStoryComment",
+        "description": "Create Comment allows you to create a Comment on any Story.",
+        "summary": "Create Story Comment"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/comments/{comment-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/StoryComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getStoryComment",
+        "description": "Get Comment is used to get Comment information.",
+        "summary": "Get Story Comment"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateStoryComment",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateStoryComment"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/StoryComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateStoryComment",
+        "description": "Update Comment replaces the text of the existing Comment.",
+        "summary": "Update Story Comment"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteStoryComment",
+        "description": "Delete a Comment from any story.",
+        "summary": "Delete Story Comment"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/comments/{comment-public-id}/reactions": {
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateOrDeleteStoryReaction",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateOrDeleteStoryReaction"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StoryReaction"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createStoryReaction",
+        "description": "Create a reaction to a story comment.",
+        "summary": "Create Story Reaction"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Comment is in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateOrDeleteStoryReaction",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateOrDeleteStoryReaction"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteStoryReaction",
+        "description": "Delete a reaction from any story comment.",
+        "summary": "Delete Story Reaction"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/comments/{comment-public-id}/unlink-from-slack": {
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story to unlink.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "comment-public-id",
+            "description": "The ID of the Comment to unlink.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/StoryComment"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "unlinkCommentThreadFromSlack",
+        "description": "Unlinks a Comment from its linked Slack thread (Comment replies and Slack replies will no longer be synced)",
+        "summary": "Unlink Comment thread from Slack"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/history": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/History"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "storyHistory",
+        "summary": "Story History"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/sub-tasks": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/StorySlim"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listStorySubTasks",
+        "description": "List Story Sub tasks returns a list of all Sub-task Stories for a given parent Story.",
+        "summary": "List Story Sub tasks"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/tasks": {
+      "post": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The ID of the Story that the Task will be in.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "CreateTask",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateTask"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/Task"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createTask",
+        "description": "Create Task is used to create a new task in a Story.",
+        "summary": "Create Task"
+      }
+    },
+    "/api/v3/stories/{story-public-id}/tasks/{task-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The unique ID of the Story this Task is associated with.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "task-public-id",
+            "description": "The unique ID of the Task.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Task"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getTask",
+        "description": "Returns information about a chosen Task.",
+        "summary": "Get Task"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The unique identifier of the parent Story.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "task-public-id",
+            "description": "The unique identifier of the Task you wish to update.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateTask",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateTask"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Task"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateTask",
+        "description": "Update Task can be used to update Task properties.",
+        "summary": "Update Task"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-public-id",
+            "description": "The unique ID of the Story this Task is associated with.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "path",
+            "name": "task-public-id",
+            "description": "The unique ID of the Task.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteTask",
+        "description": "Delete Task can be used to delete any previously created Task on a Story.",
+        "summary": "Delete Task"
+      }
+    },
+    "/api/v3/story-links": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "CreateStoryLink",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateStoryLink"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "schema": {
+              "$ref": "#/definitions/StoryLink"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "createStoryLink",
+        "description": "Story Links (called Story Relationships in the UI) allow you create semantic relationships between two stories. The parameters read like an active voice grammatical sentence:  subject -> verb -> object.\n\nThe subject story acts on the object Story; the object story is the direct object of the sentence.\n\nThe subject story \"blocks\", \"duplicates\", or \"relates to\" the object story.  Examples:\n- \"story 5 blocks story 6” -- story 6 is now \"blocked\" until story 5 is moved to a Done workflow state.\n- \"story 2 duplicates story 1” -- Story 2 represents the same body of work as Story 1 (and should probably be archived).\n- \"story 7 relates to story 3”",
+        "summary": "Create Story Link"
+      }
+    },
+    "/api/v3/story-links/{story-link-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-link-public-id",
+            "description": "The unique ID of the Story Link.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/StoryLink"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getStoryLink",
+        "description": "Returns the stories and their relationship for the given Story Link.",
+        "summary": "Get Story Link"
+      },
+      "put": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-link-public-id",
+            "description": "The unique ID of the Story Link.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "in": "body",
+            "name": "UpdateStoryLink",
+            "description": "",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateStoryLink"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/StoryLink"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "updateStoryLink",
+        "description": "Updates the stories and/or the relationship for the given Story Link.",
+        "summary": "Update Story Link"
+      },
+      "delete": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "story-link-public-id",
+            "description": "The unique ID of the Story Link.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "No Content"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "deleteStoryLink",
+        "description": "Removes the relationship between the stories for the given Story Link.",
+        "summary": "Delete Story Link"
+      }
+    },
+    "/api/v3/workflows": {
+      "get": {
+        "responses": {
+          "200": {
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/Workflow"
+              }
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "listWorkflows",
+        "description": "Returns a list of all Workflows in the Workspace.",
+        "summary": "List Workflows"
+      }
+    },
+    "/api/v3/workflows/{workflow-public-id}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "workflow-public-id",
+            "description": "The ID of the Workflow.",
+            "required": true,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "responses": {
+          "200": {
+            "schema": {
+              "$ref": "#/definitions/Workflow"
+            },
+            "description": "Resource"
+          },
+          "400": {
+            "description": "Schema mismatch"
+          },
+          "404": {
+            "description": "Resource does not exist"
+          },
+          "422": {
+            "description": "Unprocessable"
+          }
+        },
+        "operationId": "getWorkflow",
+        "description": "Get Workflow returns information about a chosen Workflow.",
+        "summary": "Get Workflow"
+      }
+    }
+  },
+  "consumes": [
+    "application/json"
+  ],
+  "host": "api.app.shortcut.com",
+  "info": {
+    "title": "Shortcut API",
+    "version": "3.0",
+    "description": "Shortcut API"
+  },
+  "swagger": "2.0",
+  "basePath": "/"
+}
\ No newline at end of file
blob - /dev/null
blob + c4a221caca72e8005e3a7dc221819ae1c1f3080a (mode 644)
--- /dev/null
+++ vendor/VENDOR
@@ -0,0 +1,11 @@
+http.rs
+	origin:	ssh://ijanc@ijanc.org/http
+	commit:	ab6adf11f97f5c70778cda19fdf34a15f4db3e59
+	date:	2026-04-25
+
+jackson.rs
+	origin:	ssh://ijanc@ijanc.org/jackson
+	commit:	99beb4a5385f52e7b353501b0ee08f0e6855d8f2
+	date:	2026-04-25
+
+Managed by marmita(1).  Use 'marmita update' to refresh.
blob - /dev/null
blob + 99a6439675d13748503e7c3b7587c40b001bc369 (mode 644)
--- /dev/null
+++ vendor/http.mk
@@ -0,0 +1,25 @@
+#
+# Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+# Build-time metadata for consumers that vendor http.rs.  Read by
+# marmita(1) on `add`/`update` and copied to vendor/<stem>.mk so the
+# consumer's Makefile can pick it up via -include $(wildcard vendor/*.mk).
+#
+# LINK_FLAGS: native libs added to the bin link step.
+# BUILD_ENV:  env vars exported when compiling vendor/http.rs as rlib
+#             (http.rs reads HTTP_VERSION via env! at compile time).
+LINK_FLAGS += -ltls -lcrypto
+BUILD_ENV  += HTTP_VERSION=$(VERSION)
blob - /dev/null
blob + 298a17645c08fc2a0326c40e97efe0179afffdb8 (mode 644)
--- /dev/null
+++ vendor/http.rs
@@ -0,0 +1,2043 @@
+// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et :
+//
+// Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+//! Minimal HTTP/1.0 and HTTP/1.1 library.
+//!
+//! Conforms to RFC 1945 (HTTP/1.0) and RFC 2616 (HTTP/1.1).  Provides a
+//! blocking client with TLS via LibreSSL's `libtls`, together with the
+//! underlying protocol primitives: header parsing, chunked transfer-
+//! coding, and message framing.
+//!
+//! The crate links `-ltls` unconditionally.
+//!
+//! # Example
+//!
+//! ```no_run
+//! let resp = http::get("https://example.com/").send()?;
+//! let body = resp.body_string()?;
+//! # Ok::<(), http::Error>(())
+//! ```
+
+use std::error;
+use std::ffi::{CStr, CString};
+use std::fmt;
+use std::fmt::Write as _;
+use std::io::{self, BufRead, BufReader, Read, Take, Write};
+use std::net::TcpStream;
+use std::os::fd::AsRawFd;
+use std::os::raw::c_void;
+use std::result;
+use std::str;
+use std::time::Duration;
+
+/// Library version
+pub const VERSION: &str = env!("HTTP_VERSION");
+
+const DEFAULT_TIMEOUT_SECS: u64 = 30;
+const MAX_HEAD_BYTES: usize = 64 * 1024;
+
+//////////////////////////////////////////////////////////////////////////////
+// Error
+//////////////////////////////////////////////////////////////////////////////
+
+/// A library error.  Carries a message and an optional byte position for
+/// parse errors.
+pub struct Error {
+    msg: String,
+    pos: usize,
+}
+
+impl Error {
+    pub fn new(msg: impl Into<String>) -> Self {
+        Self {
+            msg: msg.into(),
+            pos: 0,
+        }
+    }
+
+    pub fn at(msg: impl Into<String>, pos: usize) -> Self {
+        Self {
+            msg: msg.into(),
+            pos,
+        }
+    }
+
+    pub fn message(&self) -> &str {
+        &self.msg
+    }
+
+    pub fn position(&self) -> usize {
+        self.pos
+    }
+}
+
+impl fmt::Debug for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Error")
+            .field("msg", &self.msg)
+            .field("pos", &self.pos)
+            .finish()
+    }
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if self.pos == 0 {
+            f.write_str(&self.msg)
+        } else {
+            write!(f, "{} at byte {}", self.msg, self.pos)
+        }
+    }
+}
+
+impl error::Error for Error {}
+
+impl From<io::Error> for Error {
+    fn from(e: io::Error) -> Self {
+        Self::new(format!("io: {e}"))
+    }
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+//////////////////////////////////////////////////////////////////////////////
+// Method
+//////////////////////////////////////////////////////////////////////////////
+
+/// An HTTP request method.  RFC 2616 §5.1.1.  Extension methods are
+/// preserved in the `Other` variant.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Method {
+    Get,
+    Head,
+    Post,
+    Put,
+    Delete,
+    Options,
+    Trace,
+    Connect,
+    Patch,
+    Other(String),
+}
+
+impl Method {
+    pub fn as_str(&self) -> &str {
+        match self {
+            Self::Get => "GET",
+            Self::Head => "HEAD",
+            Self::Post => "POST",
+            Self::Put => "PUT",
+            Self::Delete => "DELETE",
+            Self::Options => "OPTIONS",
+            Self::Trace => "TRACE",
+            Self::Connect => "CONNECT",
+            Self::Patch => "PATCH",
+            Self::Other(s) => s.as_str(),
+        }
+    }
+
+    fn from_bytes(b: &[u8]) -> Self {
+        match b {
+            b"GET" => Self::Get,
+            b"HEAD" => Self::Head,
+            b"POST" => Self::Post,
+            b"PUT" => Self::Put,
+            b"DELETE" => Self::Delete,
+            b"OPTIONS" => Self::Options,
+            b"TRACE" => Self::Trace,
+            b"CONNECT" => Self::Connect,
+            b"PATCH" => Self::Patch,
+            other => Self::Other(
+                str::from_utf8(other).expect("token is ASCII").to_owned(),
+            ),
+        }
+    }
+}
+
+impl fmt::Display for Method {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Version
+//////////////////////////////////////////////////////////////////////////////
+
+/// An HTTP protocol version.  RFC 1945 §3.1, RFC 2616 §3.1.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Version {
+    Http10,
+    Http11,
+}
+
+impl Version {
+    pub fn as_str(self) -> &'static str {
+        match self {
+            Self::Http10 => "HTTP/1.0",
+            Self::Http11 => "HTTP/1.1",
+        }
+    }
+}
+
+impl fmt::Display for Version {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Status
+//////////////////////////////////////////////////////////////////////////////
+
+/// An HTTP status code.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct Status(pub u16);
+
+impl Status {
+    pub const fn code(self) -> u16 {
+        self.0
+    }
+
+    pub const fn is_informational(self) -> bool {
+        self.0 >= 100 && self.0 < 200
+    }
+
+    pub const fn is_success(self) -> bool {
+        self.0 >= 200 && self.0 < 300
+    }
+
+    pub const fn is_redirection(self) -> bool {
+        self.0 >= 300 && self.0 < 400
+    }
+
+    pub const fn is_client_error(self) -> bool {
+        self.0 >= 400 && self.0 < 500
+    }
+
+    pub const fn is_server_error(self) -> bool {
+        self.0 >= 500 && self.0 < 600
+    }
+
+    /// Canonical reason phrase from RFC 2616 §6.1.1.  Returns `None` for
+    /// unregistered codes.
+    pub const fn canonical_reason(self) -> Option<&'static str> {
+        Some(match self.0 {
+            100 => "Continue",
+            101 => "Switching Protocols",
+            200 => "OK",
+            201 => "Created",
+            202 => "Accepted",
+            203 => "Non-Authoritative Information",
+            204 => "No Content",
+            205 => "Reset Content",
+            206 => "Partial Content",
+            300 => "Multiple Choices",
+            301 => "Moved Permanently",
+            302 => "Found",
+            303 => "See Other",
+            304 => "Not Modified",
+            305 => "Use Proxy",
+            307 => "Temporary Redirect",
+            400 => "Bad Request",
+            401 => "Unauthorized",
+            402 => "Payment Required",
+            403 => "Forbidden",
+            404 => "Not Found",
+            405 => "Method Not Allowed",
+            406 => "Not Acceptable",
+            407 => "Proxy Authentication Required",
+            408 => "Request Timeout",
+            409 => "Conflict",
+            410 => "Gone",
+            411 => "Length Required",
+            412 => "Precondition Failed",
+            413 => "Request Entity Too Large",
+            414 => "Request-URI Too Long",
+            415 => "Unsupported Media Type",
+            416 => "Requested Range Not Satisfiable",
+            417 => "Expectation Failed",
+            500 => "Internal Server Error",
+            501 => "Not Implemented",
+            502 => "Bad Gateway",
+            503 => "Service Unavailable",
+            504 => "Gateway Timeout",
+            505 => "HTTP Version Not Supported",
+            _ => return None,
+        })
+    }
+}
+
+impl fmt::Display for Status {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Headers
+//////////////////////////////////////////////////////////////////////////////
+
+/// A case-insensitive, order-preserving map of header fields.
+#[derive(Clone, Debug, Default)]
+pub struct Headers {
+    entries: Vec<(String, String)>,
+}
+
+impl Headers {
+    pub const fn new() -> Self {
+        Self {
+            entries: Vec::new(),
+        }
+    }
+
+    /// Append a field.  Multiple fields with the same name are allowed
+    /// per RFC 2616 §4.2.
+    pub fn append<N: Into<String>, V: Into<String>>(
+        &mut self,
+        name: N,
+        value: V,
+    ) {
+        self.entries.push((name.into(), value.into()));
+    }
+
+    /// Remove any existing fields with the given name, then append one.
+    pub fn set<N: Into<String>, V: Into<String>>(&mut self, name: N, value: V) {
+        let name = name.into();
+        self.entries.retain(|(n, _)| !ascii_eq(n, &name));
+        self.entries.push((name, value.into()));
+    }
+
+    pub fn get(&self, name: &str) -> Option<&str> {
+        self.entries
+            .iter()
+            .find(|(n, _)| ascii_eq(n, name))
+            .map(|(_, v)| v.as_str())
+    }
+
+    pub fn get_all<'a>(
+        &'a self,
+        name: &'a str,
+    ) -> impl Iterator<Item = &'a str> {
+        self.entries
+            .iter()
+            .filter(move |(n, _)| ascii_eq(n, name))
+            .map(|(_, v)| v.as_str())
+    }
+
+    pub fn contains(&self, name: &str) -> bool {
+        self.get(name).is_some()
+    }
+
+    pub fn remove(&mut self, name: &str) {
+        self.entries.retain(|(n, _)| !ascii_eq(n, name));
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
+        self.entries.iter().map(|(n, v)| (n.as_str(), v.as_str()))
+    }
+
+    pub fn len(&self) -> usize {
+        self.entries.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+}
+
+fn ascii_eq(a: &str, b: &str) -> bool {
+    a.len() == b.len()
+        && a.bytes()
+            .zip(b.bytes())
+            .all(|(x, y)| x.eq_ignore_ascii_case(&y))
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Low-level parsing primitives
+//////////////////////////////////////////////////////////////////////////////
+
+const fn is_token(b: u8) -> bool {
+    // RFC 2616 §2.2: token = 1*<any CHAR except CTLs or separators>.
+    matches!(
+        b,
+        b'!' | b'#'
+            | b'$'
+            | b'%'
+            | b'&'
+            | b'\''
+            | b'*'
+            | b'+'
+            | b'-'
+            | b'.'
+            | b'0'..=b'9'
+            | b'A'..=b'Z'
+            | b'^'
+            | b'_'
+            | b'`'
+            | b'a'..=b'z'
+            | b'|'
+            | b'~'
+    )
+}
+
+struct Parser<'a> {
+    buf: &'a [u8],
+    pos: usize,
+}
+
+impl<'a> Parser<'a> {
+    fn new(buf: &'a [u8]) -> Self {
+        Self { buf, pos: 0 }
+    }
+
+    fn peek(&self) -> Option<u8> {
+        self.buf.get(self.pos).copied()
+    }
+
+    fn err<T>(&self, msg: &'static str) -> Result<T> {
+        Err(Error::at(msg, self.pos))
+    }
+
+    fn expect(&mut self, bytes: &[u8]) -> Result<()> {
+        if self.buf[self.pos..].starts_with(bytes) {
+            self.pos += bytes.len();
+            Ok(())
+        } else {
+            self.err("expected literal")
+        }
+    }
+
+    fn expect_sp(&mut self) -> Result<()> {
+        match self.peek() {
+            Some(b' ') => {
+                self.pos += 1;
+                Ok(())
+            }
+            _ => self.err("expected SP"),
+        }
+    }
+
+    /// Consume CRLF, or a bare LF per RFC 2616 §19.3 robustness.
+    fn eat_crlf(&mut self) -> Result<()> {
+        match self.peek() {
+            Some(b'\r') if self.buf.get(self.pos + 1) == Some(&b'\n') => {
+                self.pos += 2;
+                Ok(())
+            }
+            Some(b'\n') => {
+                self.pos += 1;
+                Ok(())
+            }
+            _ => self.err("expected CRLF"),
+        }
+    }
+
+    fn skip_ws(&mut self) {
+        while matches!(self.peek(), Some(b' ') | Some(b'\t')) {
+            self.pos += 1;
+        }
+    }
+
+    fn take_token(&mut self) -> Result<&'a [u8]> {
+        let start = self.pos;
+        while let Some(b) = self.peek() {
+            if !is_token(b) {
+                break;
+            }
+            self.pos += 1;
+        }
+        if self.pos == start {
+            return self.err("expected token");
+        }
+        Ok(&self.buf[start..self.pos])
+    }
+
+    fn take_until_ws(&mut self) -> Result<&'a [u8]> {
+        let start = self.pos;
+        while let Some(b) = self.peek() {
+            if matches!(b, b' ' | b'\t' | b'\r' | b'\n') {
+                break;
+            }
+            self.pos += 1;
+        }
+        if self.pos == start {
+            return self.err("expected non-whitespace");
+        }
+        Ok(&self.buf[start..self.pos])
+    }
+
+    fn take_until_eol(&mut self) -> &'a [u8] {
+        let start = self.pos;
+        while let Some(b) = self.peek() {
+            if b == b'\r' || b == b'\n' {
+                break;
+            }
+            self.pos += 1;
+        }
+        &self.buf[start..self.pos]
+    }
+}
+
+fn parse_version(p: &mut Parser<'_>) -> Result<Version> {
+    p.expect(b"HTTP/")?;
+    let maj = parse_u16(p)?;
+    p.expect(b".")?;
+    let min = parse_u16(p)?;
+    match (maj, min) {
+        (1, 0) => Ok(Version::Http10),
+        (1, 1) => Ok(Version::Http11),
+        _ => p.err("unsupported HTTP version"),
+    }
+}
+
+fn parse_u16(p: &mut Parser<'_>) -> Result<u16> {
+    let start = p.pos;
+    let mut n: u32 = 0;
+    while let Some(b) = p.peek() {
+        if !b.is_ascii_digit() {
+            break;
+        }
+        n = n * 10 + (b - b'0') as u32;
+        if n > u16::MAX as u32 {
+            return p.err("numeric overflow");
+        }
+        p.pos += 1;
+    }
+    if p.pos == start {
+        return p.err("expected digit");
+    }
+    Ok(n as u16)
+}
+
+fn parse_status_code(p: &mut Parser<'_>) -> Result<u16> {
+    let mut d = [0u16; 3];
+    for slot in &mut d {
+        match p.peek() {
+            Some(b) if b.is_ascii_digit() => {
+                *slot = (b - b'0') as u16;
+                p.pos += 1;
+            }
+            _ => return p.err("expected 3-digit status code"),
+        }
+    }
+    Ok(d[0] * 100 + d[1] * 10 + d[2])
+}
+
+fn parse_headers(p: &mut Parser<'_>) -> Result<Headers> {
+    let mut headers = Headers::new();
+    loop {
+        if matches!(p.peek(), Some(b'\r') | Some(b'\n')) {
+            p.eat_crlf()?;
+            return Ok(headers);
+        }
+        let name = p.take_token()?;
+        let name = str::from_utf8(name)
+            .expect("field-name is ASCII")
+            .to_owned();
+
+        if p.peek() != Some(b':') {
+            return p.err("expected ':' after field-name");
+        }
+        p.pos += 1;
+        p.skip_ws();
+
+        let mut value = p.take_until_eol().to_vec();
+        p.eat_crlf()?;
+
+        // obs-fold: continuation lines begin with SP or HT.
+        while matches!(p.peek(), Some(b' ') | Some(b'\t')) {
+            p.skip_ws();
+            let cont = p.take_until_eol();
+            if !value.is_empty() && !cont.is_empty() {
+                value.push(b' ');
+            }
+            value.extend_from_slice(cont);
+            p.eat_crlf()?;
+        }
+
+        while matches!(value.last(), Some(b' ') | Some(b'\t')) {
+            value.pop();
+        }
+
+        let value = String::from_utf8(value)
+            .map_err(|_| Error::at("invalid UTF-8 in field-value", p.pos))?;
+        headers.append(name, value);
+    }
+}
+
+/// Parse an HTTP request head (request-line + header block terminated
+/// by an empty line).  Returns the decoded request and the number of
+/// bytes consumed.  RFC 2616 §5.
+pub fn parse_request(buf: &[u8]) -> Result<(Request, usize)> {
+    let mut p = Parser::new(buf);
+
+    let method = Method::from_bytes(p.take_token()?);
+    p.expect_sp()?;
+
+    let target = p.take_until_ws()?;
+    let target = str::from_utf8(target)
+        .map_err(|_| Error::at("invalid UTF-8 in request-target", p.pos))?
+        .to_owned();
+    p.expect_sp()?;
+
+    let version = parse_version(&mut p)?;
+    p.eat_crlf()?;
+
+    let headers = parse_headers(&mut p)?;
+    Ok((
+        Request {
+            method,
+            target,
+            version,
+            headers,
+        },
+        p.pos,
+    ))
+}
+
+/// Parse an HTTP response head (status-line + header block).
+pub fn parse_response_head(buf: &[u8]) -> Result<(ResponseHead, usize)> {
+    let mut p = Parser::new(buf);
+
+    let version = parse_version(&mut p)?;
+    p.expect_sp()?;
+
+    let code = parse_status_code(&mut p)?;
+    p.expect_sp()?;
+
+    let reason = p.take_until_eol();
+    let reason = str::from_utf8(reason)
+        .map_err(|_| Error::at("invalid UTF-8 in reason-phrase", p.pos))?
+        .trim()
+        .to_owned();
+    p.eat_crlf()?;
+
+    let headers = parse_headers(&mut p)?;
+    Ok((
+        ResponseHead {
+            version,
+            status: Status(code),
+            reason,
+            headers,
+        },
+        p.pos,
+    ))
+}
+
+/// Read an HTTP message head (everything up to and including the empty
+/// line separating the body).  The BufReader is positioned at the first
+/// body byte on return.
+fn read_head<R: BufRead>(r: &mut R) -> Result<Vec<u8>> {
+    let mut buf = Vec::with_capacity(2048);
+    loop {
+        let start = buf.len();
+        let n = r.read_until(b'\n', &mut buf)?;
+        if n == 0 {
+            return Err(Error::new("unexpected EOF reading head"));
+        }
+        if buf.len() > MAX_HEAD_BYTES {
+            return Err(Error::new("head too large"));
+        }
+        let line = &buf[start..];
+        if line == b"\r\n" || line == b"\n" {
+            return Ok(buf);
+        }
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Request (data) and ResponseHead
+//////////////////////////////////////////////////////////////////////////////
+
+/// A parsed HTTP request message head.
+#[derive(Clone, Debug)]
+pub struct Request {
+    pub method: Method,
+    pub target: String,
+    pub version: Version,
+    pub headers: Headers,
+}
+
+impl Request {
+    /// Serialize the request-line + headers to `w`.  The caller writes
+    /// any body bytes separately.
+    pub fn write_head(&self, w: &mut impl Write) -> io::Result<()> {
+        write!(w, "{} {} {}\r\n", self.method, self.target, self.version)?;
+        write_headers(w, &self.headers)
+    }
+}
+
+/// A parsed HTTP response message head.
+#[derive(Clone, Debug)]
+pub struct ResponseHead {
+    pub version: Version,
+    pub status: Status,
+    pub reason: String,
+    pub headers: Headers,
+}
+
+impl ResponseHead {
+    pub fn write_head(&self, w: &mut impl Write) -> io::Result<()> {
+        write!(w, "{} {} {}\r\n", self.version, self.status, self.reason)?;
+        write_headers(w, &self.headers)
+    }
+}
+
+fn write_headers(w: &mut impl Write, h: &Headers) -> io::Result<()> {
+    for (name, value) in h.iter() {
+        write!(w, "{}: {}\r\n", name, value)?;
+    }
+    w.write_all(b"\r\n")
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Body framing (RFC 2616 §4.4)
+//////////////////////////////////////////////////////////////////////////////
+
+/// Framing rule for a message body.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum BodyLength {
+    /// No body — 1xx/204/304 responses, or response to HEAD, or a
+    /// request without Content-Length or Transfer-Encoding.
+    Empty,
+    /// Body of exactly N bytes (Content-Length).
+    Fixed(u64),
+    /// Chunked transfer-coding (RFC 2616 §3.6.1).
+    Chunked,
+    /// Body delimited by connection close (responses only).
+    CloseDelimited,
+}
+
+/// Determine the framing for a response body given the request method,
+/// response status, and response headers.  RFC 2616 §4.4.
+pub fn response_body_length(
+    request_method: &Method,
+    status: Status,
+    headers: &Headers,
+) -> Result<BodyLength> {
+    if *request_method == Method::Head
+        || status.is_informational()
+        || status.0 == 204
+        || status.0 == 304
+    {
+        return Ok(BodyLength::Empty);
+    }
+    body_length_from_headers(headers, false)
+}
+
+/// Determine the framing for a request body given request headers.
+pub fn request_body_length(headers: &Headers) -> Result<BodyLength> {
+    body_length_from_headers(headers, true)
+}
+
+fn body_length_from_headers(
+    headers: &Headers,
+    is_request: bool,
+) -> Result<BodyLength> {
+    if let Some(te) = headers.get("Transfer-Encoding") {
+        if is_final_chunked(te) {
+            return Ok(BodyLength::Chunked);
+        }
+        return Err(Error::new("unsupported Transfer-Encoding"));
+    }
+    if let Some(cl) = headers.get("Content-Length") {
+        let n: u64 = cl
+            .trim()
+            .parse()
+            .map_err(|_| Error::new("invalid Content-Length"))?;
+        return Ok(BodyLength::Fixed(n));
+    }
+    if is_request {
+        Ok(BodyLength::Empty)
+    } else {
+        Ok(BodyLength::CloseDelimited)
+    }
+}
+
+fn is_final_chunked(te: &str) -> bool {
+    te.rsplit(',')
+        .next()
+        .map(|s| s.trim().eq_ignore_ascii_case("chunked"))
+        .unwrap_or(false)
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Chunked transfer-coding (RFC 2616 §3.6.1)
+//////////////////////////////////////////////////////////////////////////////
+
+/// A `Read` adapter that decodes chunked transfer-coding.
+pub struct ChunkedReader<R: BufRead> {
+    inner: R,
+    state: ChunkState,
+}
+
+enum ChunkState {
+    /// Next byte starts a chunk-size line.
+    Size,
+    /// Currently inside a chunk body; N bytes remain.
+    Body(u64),
+    /// Chunk body exhausted; consume trailing CRLF before next size.
+    Tail,
+    /// Zero-length chunk seen; trailer consumed; stream at EOF.
+    Done,
+}
+
+impl<R: BufRead> ChunkedReader<R> {
+    pub fn new(inner: R) -> Self {
+        Self {
+            inner,
+            state: ChunkState::Size,
+        }
+    }
+
+    pub fn into_inner(self) -> R {
+        self.inner
+    }
+}
+
+impl<R: BufRead> Read for ChunkedReader<R> {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        loop {
+            match &mut self.state {
+                ChunkState::Done => return Ok(0),
+                ChunkState::Tail => {
+                    read_crlf(&mut self.inner)?;
+                    self.state = ChunkState::Size;
+                }
+                ChunkState::Size => {
+                    let size = read_chunk_size(&mut self.inner)?;
+                    if size == 0 {
+                        read_trailer(&mut self.inner)?;
+                        self.state = ChunkState::Done;
+                        return Ok(0);
+                    }
+                    self.state = ChunkState::Body(size);
+                }
+                ChunkState::Body(remaining) => {
+                    if buf.is_empty() {
+                        return Ok(0);
+                    }
+                    let want = buf.len().min(*remaining as usize);
+                    let n = self.inner.read(&mut buf[..want])?;
+                    if n == 0 {
+                        return Err(io::Error::new(
+                            io::ErrorKind::UnexpectedEof,
+                            "chunk truncated",
+                        ));
+                    }
+                    *remaining -= n as u64;
+                    if *remaining == 0 {
+                        self.state = ChunkState::Tail;
+                    }
+                    return Ok(n);
+                }
+            }
+        }
+    }
+}
+
+fn read_chunk_size<R: BufRead>(r: &mut R) -> io::Result<u64> {
+    let line = read_line_vec(r)?;
+    let end = line.iter().position(|&b| b == b';').unwrap_or(line.len());
+    let hex = str::from_utf8(&line[..end])
+        .map_err(|_| invalid("non-ASCII chunk size"))?
+        .trim();
+    u64::from_str_radix(hex, 16).map_err(|_| invalid("invalid chunk size"))
+}
+
+fn read_trailer<R: BufRead>(r: &mut R) -> io::Result<()> {
+    loop {
+        let line = read_line_vec(r)?;
+        if line.is_empty() {
+            return Ok(());
+        }
+    }
+}
+
+fn read_crlf<R: BufRead>(r: &mut R) -> io::Result<()> {
+    let line = read_line_vec(r)?;
+    if line.is_empty() {
+        Ok(())
+    } else {
+        Err(invalid("expected CRLF"))
+    }
+}
+
+fn read_line_vec<R: BufRead>(r: &mut R) -> io::Result<Vec<u8>> {
+    let mut buf = Vec::new();
+    let n = r.read_until(b'\n', &mut buf)?;
+    if n == 0 {
+        return Err(io::Error::new(
+            io::ErrorKind::UnexpectedEof,
+            "unexpected EOF",
+        ));
+    }
+    if buf.last() == Some(&b'\n') {
+        buf.pop();
+    }
+    if buf.last() == Some(&b'\r') {
+        buf.pop();
+    }
+    Ok(buf)
+}
+
+fn invalid(msg: &'static str) -> io::Error {
+    io::Error::new(io::ErrorKind::InvalidData, msg)
+}
+
+/// A `Write` adapter that encodes output in chunked transfer-coding.
+/// Call [`finish`](Self::finish) to emit the terminating zero-chunk;
+/// dropping without calling `finish` leaves the stream incomplete.
+pub struct ChunkedWriter<W: Write> {
+    inner: W,
+}
+
+impl<W: Write> ChunkedWriter<W> {
+    pub fn new(inner: W) -> Self {
+        Self { inner }
+    }
+
+    pub fn finish(mut self) -> io::Result<W> {
+        self.inner.write_all(b"0\r\n\r\n")?;
+        Ok(self.inner)
+    }
+}
+
+impl<W: Write> Write for ChunkedWriter<W> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        if buf.is_empty() {
+            return Ok(0);
+        }
+        write!(self.inner, "{:x}\r\n", buf.len())?;
+        self.inner.write_all(buf)?;
+        self.inner.write_all(b"\r\n")?;
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.inner.flush()
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// URL parsing (internal)
+//////////////////////////////////////////////////////////////////////////////
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum Scheme {
+    Http,
+    Https,
+}
+
+fn parse_url(url: &str) -> Result<(Scheme, &str, u16, &str)> {
+    let (scheme, rest) = if let Some(r) = url.strip_prefix("https://") {
+        (Scheme::Https, r)
+    } else if let Some(r) = url.strip_prefix("http://") {
+        (Scheme::Http, r)
+    } else {
+        return Err(Error::new("URL must start with http:// or https://"));
+    };
+
+    let (authority, path) = match rest.find('/') {
+        Some(i) => (&rest[..i], &rest[i..]),
+        None => (rest, "/"),
+    };
+
+    if authority.is_empty() {
+        return Err(Error::new("URL missing host"));
+    }
+
+    let (host, port) = match authority.rsplit_once(':') {
+        Some((h, p)) => {
+            let port: u16 =
+                p.parse().map_err(|_| Error::new("invalid port in URL"))?;
+            (h, port)
+        }
+        None => (
+            authority,
+            match scheme {
+                Scheme::Http => 80,
+                Scheme::Https => 443,
+            },
+        ),
+    };
+
+    Ok((scheme, host, port, path))
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// libtls FFI
+//////////////////////////////////////////////////////////////////////////////
+
+mod ffi {
+    use std::os::raw::{c_char, c_int, c_void};
+
+    #[repr(C)]
+    pub struct Tls {
+        _p: [u8; 0],
+    }
+    #[repr(C)]
+    pub struct TlsConfig {
+        _p: [u8; 0],
+    }
+
+    pub const TLS_WANT_POLLIN: isize = -2;
+    pub const TLS_WANT_POLLOUT: isize = -3;
+
+    #[link(name = "tls")]
+    unsafe extern "C" {
+        pub fn tls_init() -> c_int;
+        pub fn tls_config_new() -> *mut TlsConfig;
+        pub fn tls_config_free(c: *mut TlsConfig);
+        pub fn tls_client() -> *mut Tls;
+        pub fn tls_configure(ctx: *mut Tls, cfg: *mut TlsConfig) -> c_int;
+        pub fn tls_connect_socket(
+            ctx: *mut Tls,
+            fd: c_int,
+            servername: *const c_char,
+        ) -> c_int;
+        pub fn tls_handshake(ctx: *mut Tls) -> c_int;
+        pub fn tls_write(ctx: *mut Tls, buf: *const c_void, n: usize) -> isize;
+        pub fn tls_read(ctx: *mut Tls, buf: *mut c_void, n: usize) -> isize;
+        pub fn tls_close(ctx: *mut Tls) -> c_int;
+        pub fn tls_free(ctx: *mut Tls);
+        pub fn tls_error(ctx: *mut Tls) -> *const c_char;
+    }
+}
+
+struct TlsStream {
+    ctx: *mut ffi::Tls,
+    cfg: *mut ffi::TlsConfig,
+    _sock: TcpStream,
+}
+
+impl TlsStream {
+    fn connect(host: &str, sock: TcpStream) -> Result<Self> {
+        unsafe {
+            if ffi::tls_init() != 0 {
+                return Err(Error::new("tls_init failed"));
+            }
+            let cfg = ffi::tls_config_new();
+            if cfg.is_null() {
+                return Err(Error::new("tls_config_new failed"));
+            }
+            let ctx = ffi::tls_client();
+            if ctx.is_null() {
+                ffi::tls_config_free(cfg);
+                return Err(Error::new("tls_client failed"));
+            }
+            if ffi::tls_configure(ctx, cfg) != 0 {
+                let e = tls_errmsg(ctx);
+                ffi::tls_free(ctx);
+                ffi::tls_config_free(cfg);
+                return Err(Error::new(format!("tls_configure: {e}")));
+            }
+            let chost = CString::new(host)
+                .map_err(|_| Error::new("host contains NUL"))?;
+            if ffi::tls_connect_socket(ctx, sock.as_raw_fd(), chost.as_ptr())
+                != 0
+            {
+                let e = tls_errmsg(ctx);
+                ffi::tls_free(ctx);
+                ffi::tls_config_free(cfg);
+                return Err(Error::new(format!("tls_connect_socket: {e}")));
+            }
+            loop {
+                let r = ffi::tls_handshake(ctx) as isize;
+                if r == 0 {
+                    break;
+                }
+                if r == ffi::TLS_WANT_POLLIN || r == ffi::TLS_WANT_POLLOUT {
+                    continue;
+                }
+                let e = tls_errmsg(ctx);
+                ffi::tls_free(ctx);
+                ffi::tls_config_free(cfg);
+                return Err(Error::new(format!("tls_handshake: {e}")));
+            }
+            Ok(Self {
+                ctx,
+                cfg,
+                _sock: sock,
+            })
+        }
+    }
+}
+
+impl Read for TlsStream {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        unsafe {
+            loop {
+                let r = ffi::tls_read(
+                    self.ctx,
+                    buf.as_mut_ptr() as *mut c_void,
+                    buf.len(),
+                );
+                if r == ffi::TLS_WANT_POLLIN || r == ffi::TLS_WANT_POLLOUT {
+                    continue;
+                }
+                if r < 0 {
+                    return Err(io::Error::other(format!(
+                        "tls_read: {}",
+                        tls_errmsg(self.ctx)
+                    )));
+                }
+                return Ok(r as usize);
+            }
+        }
+    }
+}
+
+impl Write for TlsStream {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        unsafe {
+            loop {
+                let r = ffi::tls_write(
+                    self.ctx,
+                    buf.as_ptr() as *const c_void,
+                    buf.len(),
+                );
+                if r == ffi::TLS_WANT_POLLIN || r == ffi::TLS_WANT_POLLOUT {
+                    continue;
+                }
+                if r < 0 {
+                    return Err(io::Error::other(format!(
+                        "tls_write: {}",
+                        tls_errmsg(self.ctx)
+                    )));
+                }
+                return Ok(r as usize);
+            }
+        }
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+impl Drop for TlsStream {
+    fn drop(&mut self) {
+        unsafe {
+            if !self.ctx.is_null() {
+                loop {
+                    let r = ffi::tls_close(self.ctx) as isize;
+                    if r == 0
+                        || (r != ffi::TLS_WANT_POLLIN
+                            && r != ffi::TLS_WANT_POLLOUT)
+                    {
+                        break;
+                    }
+                }
+                ffi::tls_free(self.ctx);
+            }
+            if !self.cfg.is_null() {
+                ffi::tls_config_free(self.cfg);
+            }
+        }
+    }
+}
+
+unsafe fn tls_errmsg(ctx: *mut ffi::Tls) -> String {
+    unsafe {
+        let p = ffi::tls_error(ctx);
+        if p.is_null() {
+            "(unknown)".into()
+        } else {
+            CStr::from_ptr(p).to_string_lossy().into_owned()
+        }
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Unified plain/TLS stream
+//////////////////////////////////////////////////////////////////////////////
+
+enum Stream {
+    Plain(TcpStream),
+    Tls(TlsStream),
+}
+
+impl Read for Stream {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        match self {
+            Self::Plain(s) => s.read(buf),
+            Self::Tls(s) => s.read(buf),
+        }
+    }
+}
+
+impl Write for Stream {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        match self {
+            Self::Plain(s) => s.write(buf),
+            Self::Tls(s) => s.write(buf),
+        }
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        match self {
+            Self::Plain(s) => s.flush(),
+            Self::Tls(s) => s.flush(),
+        }
+    }
+}
+
+fn connect(
+    scheme: Scheme,
+    host: &str,
+    port: u16,
+    timeout: Duration,
+) -> Result<Stream> {
+    let sock = TcpStream::connect((host, port))
+        .map_err(|e| Error::new(format!("connect {host}:{port}: {e}")))?;
+    let _ = sock.set_read_timeout(Some(timeout));
+    let _ = sock.set_write_timeout(Some(timeout));
+    match scheme {
+        Scheme::Http => Ok(Stream::Plain(sock)),
+        Scheme::Https => Ok(Stream::Tls(TlsStream::connect(host, sock)?)),
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Response
+//////////////////////////////////////////////////////////////////////////////
+
+/// A response from a completed HTTP request.  The body is exposed as a
+/// `Read` implementation so large or streaming responses (for example
+/// Server-Sent Events) can be consumed incrementally.
+pub struct Response {
+    pub status: Status,
+    pub version: Version,
+    pub reason: String,
+    pub headers: Headers,
+    body: Body,
+}
+
+enum Body {
+    Empty,
+    Fixed(Take<BufReader<Stream>>),
+    Chunked(ChunkedReader<BufReader<Stream>>),
+    Close(BufReader<Stream>),
+}
+
+impl Response {
+    /// Read the entire body into a `Vec<u8>`.
+    pub fn body_bytes(mut self) -> Result<Vec<u8>> {
+        let mut out = Vec::new();
+        self.read_to_end(&mut out)?;
+        Ok(out)
+    }
+
+    /// Read the entire body into a `String`, expecting valid UTF-8.
+    pub fn body_string(mut self) -> Result<String> {
+        let mut s = String::new();
+        self.read_to_string(&mut s)?;
+        Ok(s)
+    }
+}
+
+impl Read for Response {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        match &mut self.body {
+            Body::Empty => Ok(0),
+            Body::Fixed(r) => r.read(buf),
+            Body::Chunked(r) => r.read(buf),
+            Body::Close(r) => r.read(buf),
+        }
+    }
+}
+
+impl fmt::Debug for Response {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Response")
+            .field("status", &self.status)
+            .field("version", &self.version)
+            .field("reason", &self.reason)
+            .field("headers", &self.headers)
+            .finish()
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Request builder
+//////////////////////////////////////////////////////////////////////////////
+
+/// Fluent builder for outgoing requests.  Construct one via
+/// [`get`], [`post`], [`put`], [`delete`], [`head`] or [`request`].
+pub struct RequestBuilder {
+    method: Method,
+    url: String,
+    headers: Headers,
+    body: Vec<u8>,
+    timeout: Duration,
+}
+
+impl RequestBuilder {
+    fn new(method: Method, url: impl Into<String>) -> Self {
+        Self {
+            method,
+            url: url.into(),
+            headers: Headers::new(),
+            body: Vec::new(),
+            timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
+        }
+    }
+
+    /// Append a header.  Multiple calls with the same name append
+    /// multiple fields (RFC 2616 §4.2).
+    pub fn header(
+        mut self,
+        name: impl Into<String>,
+        value: impl Into<String>,
+    ) -> Self {
+        self.headers.append(name, value);
+        self
+    }
+
+    /// Set the request body.  Overwrites any previous body.  The
+    /// Content-Length header is set automatically on send unless the
+    /// caller already set one.
+    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
+        self.body = body.into();
+        self
+    }
+
+    /// Set the body from a list of form pairs, URL-encoded, and set
+    /// Content-Type to application/x-www-form-urlencoded.
+    pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
+        self.body = url_form(pairs).into_bytes();
+        if !self.headers.contains("Content-Type") {
+            self.headers
+                .set("Content-Type", "application/x-www-form-urlencoded");
+        }
+        self
+    }
+
+    /// Set the body to a `multipart/form-data` payload (RFC 2388) and
+    /// the Content-Type header (with boundary).  Overwrites any previous
+    /// body and Content-Type.
+    pub fn multipart(mut self, form: Multipart) -> Self {
+        let ct = form.content_type();
+        self.body = form.into_bytes();
+        self.headers.set("Content-Type", ct);
+        self
+    }
+
+    /// Override the read/write timeout.  Default is 30 seconds.
+    pub fn timeout(mut self, timeout: Duration) -> Self {
+        self.timeout = timeout;
+        self
+    }
+
+    /// Send the request and return the response.  The underlying
+    /// connection is closed when the response is dropped.
+    pub fn send(mut self) -> Result<Response> {
+        let (scheme, host, port, path) = parse_url(&self.url)?;
+
+        if !self.headers.contains("Host") {
+            let host_hdr = if (scheme == Scheme::Http && port == 80)
+                || (scheme == Scheme::Https && port == 443)
+            {
+                host.to_string()
+            } else {
+                format!("{host}:{port}")
+            };
+            self.headers.set("Host", host_hdr);
+        }
+        if !self.headers.contains("User-Agent") {
+            self.headers.set("User-Agent", format!("http/{VERSION}"));
+        }
+        if !self.headers.contains("Connection") {
+            self.headers.set("Connection", "close");
+        }
+        if !self.body.is_empty() && !self.headers.contains("Content-Length") {
+            self.headers
+                .set("Content-Length", self.body.len().to_string());
+        }
+
+        let stream = connect(scheme, host, port, self.timeout)?;
+        let mut stream = stream;
+
+        write!(stream, "{} {} HTTP/1.1\r\n", self.method, path)?;
+        for (name, value) in self.headers.iter() {
+            write!(stream, "{}: {}\r\n", name, value)?;
+        }
+        stream.write_all(b"\r\n")?;
+        if !self.body.is_empty() {
+            stream.write_all(&self.body)?;
+        }
+        stream.flush()?;
+
+        let mut reader = BufReader::new(stream);
+        let head = read_head(&mut reader)?;
+        let (h, _) = parse_response_head(&head)?;
+
+        let length = response_body_length(&self.method, h.status, &h.headers)?;
+        let body = match length {
+            BodyLength::Empty => Body::Empty,
+            BodyLength::Fixed(n) => Body::Fixed(reader.take(n)),
+            BodyLength::Chunked => Body::Chunked(ChunkedReader::new(reader)),
+            BodyLength::CloseDelimited => Body::Close(reader),
+        };
+
+        Ok(Response {
+            status: h.status,
+            version: h.version,
+            reason: h.reason,
+            headers: h.headers,
+            body,
+        })
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Top-level constructors
+//////////////////////////////////////////////////////////////////////////////
+
+pub fn get(url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(Method::Get, url)
+}
+
+pub fn head(url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(Method::Head, url)
+}
+
+pub fn post(url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(Method::Post, url)
+}
+
+pub fn put(url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(Method::Put, url)
+}
+
+pub fn delete(url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(Method::Delete, url)
+}
+
+pub fn patch(url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(Method::Patch, url)
+}
+
+pub fn request(method: Method, url: impl Into<String>) -> RequestBuilder {
+    RequestBuilder::new(method, url)
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// URL encoding utilities
+//////////////////////////////////////////////////////////////////////////////
+
+/// Percent-encode per RFC 3986 §2.3 (unreserved set).
+pub fn percent_encode(s: &str) -> String {
+    let mut out = String::with_capacity(s.len());
+    for &b in s.as_bytes() {
+        match b {
+            b'A'..=b'Z'
+            | b'a'..=b'z'
+            | b'0'..=b'9'
+            | b'-'
+            | b'.'
+            | b'_'
+            | b'~' => out.push(b as char),
+            _ => {
+                let _ = write!(out, "%{b:02X}");
+            }
+        }
+    }
+    out
+}
+
+/// Percent-decode.  `+` is interpreted as space (form convention).
+/// Invalid sequences are emitted literally.
+pub fn percent_decode(s: &str) -> String {
+    let bytes = s.as_bytes();
+    let mut out = Vec::with_capacity(bytes.len());
+    let mut i = 0;
+    while i < bytes.len() {
+        match bytes[i] {
+            b'+' => {
+                out.push(b' ');
+                i += 1;
+            }
+            b'%' if i + 2 < bytes.len() => {
+                let hex = str::from_utf8(&bytes[i + 1..i + 3])
+                    .ok()
+                    .and_then(|h| u8::from_str_radix(h, 16).ok());
+                match hex {
+                    Some(b) => {
+                        out.push(b);
+                        i += 3;
+                    }
+                    None => {
+                        out.push(bytes[i]);
+                        i += 1;
+                    }
+                }
+            }
+            b => {
+                out.push(b);
+                i += 1;
+            }
+        }
+    }
+    String::from_utf8(out).unwrap_or_default()
+}
+
+/// Serialize a list of key-value pairs as application/x-www-form-urlencoded.
+pub fn url_form(pairs: &[(&str, &str)]) -> String {
+    let mut out = String::new();
+    for (i, (k, v)) in pairs.iter().enumerate() {
+        if i > 0 {
+            out.push('&');
+        }
+        out.push_str(&percent_encode(k));
+        out.push('=');
+        out.push_str(&percent_encode(v));
+    }
+    out
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// multipart/form-data (RFC 2388)
+//////////////////////////////////////////////////////////////////////////////
+
+/// A `multipart/form-data` body builder.  RFC 2388.
+///
+/// Each part has a name and either a text value or a file payload
+/// (filename + content-type + bytes).  Pass the finished form to
+/// [`RequestBuilder::multipart`], which sets the Content-Type header
+/// (with the boundary) and serializes the body.
+#[derive(Clone, Debug)]
+pub struct Multipart {
+    boundary: String,
+    parts: Vec<Part>,
+}
+
+#[derive(Clone, Debug)]
+struct Part {
+    name: String,
+    filename: Option<String>,
+    content_type: Option<String>,
+    body: Vec<u8>,
+}
+
+impl Multipart {
+    /// Create a new form with a fresh boundary.
+    pub fn new() -> Self {
+        Self {
+            boundary: gen_boundary(),
+            parts: Vec::new(),
+        }
+    }
+
+    /// Append a text field.
+    pub fn text(
+        mut self,
+        name: impl Into<String>,
+        value: impl Into<String>,
+    ) -> Self {
+        self.parts.push(Part {
+            name: name.into(),
+            filename: None,
+            content_type: None,
+            body: value.into().into_bytes(),
+        });
+        self
+    }
+
+    /// Append a file field with explicit filename, content type, and bytes.
+    pub fn file(
+        mut self,
+        name: impl Into<String>,
+        filename: impl Into<String>,
+        content_type: impl Into<String>,
+        body: impl Into<Vec<u8>>,
+    ) -> Self {
+        self.parts.push(Part {
+            name: name.into(),
+            filename: Some(filename.into()),
+            content_type: Some(content_type.into()),
+            body: body.into(),
+        });
+        self
+    }
+
+    /// Append a file field by reading `path` from disk.  The filename
+    /// sent is the path's final component.
+    pub fn file_path(
+        self,
+        name: impl Into<String>,
+        path: impl AsRef<std::path::Path>,
+        content_type: impl Into<String>,
+    ) -> Result<Self> {
+        let path = path.as_ref();
+        let body = std::fs::read(path)
+            .map_err(|e| Error::new(format!("read {}: {e}", path.display())))?;
+        let filename = path
+            .file_name()
+            .and_then(|s| s.to_str())
+            .ok_or_else(|| Error::new("path has no file name"))?
+            .to_owned();
+        Ok(self.file(name, filename, content_type, body))
+    }
+
+    /// The boundary string (without the leading dashes).
+    pub fn boundary(&self) -> &str {
+        &self.boundary
+    }
+
+    /// Value to use in the `Content-Type` header.
+    pub fn content_type(&self) -> String {
+        format!("multipart/form-data; boundary={}", self.boundary)
+    }
+
+    /// Serialize the form body to bytes.
+    pub fn into_bytes(self) -> Vec<u8> {
+        let mut out = Vec::new();
+        self.write_to(&mut out).expect("Vec write is infallible");
+        out
+    }
+
+    /// Write the serialized form body to `w`.
+    pub fn write_to(&self, w: &mut impl Write) -> io::Result<()> {
+        for part in &self.parts {
+            write!(w, "--{}\r\n", self.boundary)?;
+            w.write_all(b"Content-Disposition: form-data; name=\"")?;
+            write_quoted(w, &part.name)?;
+            w.write_all(b"\"")?;
+            if let Some(filename) = &part.filename {
+                w.write_all(b"; filename=\"")?;
+                write_quoted(w, filename)?;
+                w.write_all(b"\"")?;
+            }
+            w.write_all(b"\r\n")?;
+            if let Some(ct) = &part.content_type {
+                write!(w, "Content-Type: {ct}\r\n")?;
+            }
+            w.write_all(b"\r\n")?;
+            w.write_all(&part.body)?;
+            w.write_all(b"\r\n")?;
+        }
+        write!(w, "--{}--\r\n", self.boundary)
+    }
+}
+
+impl Default for Multipart {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Backslash-escape `"` and `\` per RFC 2616 §2.2 quoted-string.  CR/LF
+/// are not legal inside a quoted-string, so they are dropped.
+fn write_quoted(w: &mut impl Write, s: &str) -> io::Result<()> {
+    for &b in s.as_bytes() {
+        match b {
+            b'"' | b'\\' => w.write_all(&[b'\\', b])?,
+            b'\r' | b'\n' => continue,
+            _ => w.write_all(&[b])?,
+        }
+    }
+    Ok(())
+}
+
+/// A boundary that is unique within a process: nanos + pid + counter,
+/// as 70-bchar-safe hex.  RFC 2046 §5.1.1 only requires uniqueness
+/// against the body, not unpredictability.
+fn gen_boundary() -> String {
+    use std::sync::atomic::{AtomicU64, Ordering};
+    static COUNTER: AtomicU64 = AtomicU64::new(0);
+    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
+    let nanos = std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .map(|d| d.as_nanos() as u64)
+        .unwrap_or(0);
+    let pid = std::process::id() as u64;
+    format!("----http-rs-{nanos:016x}-{pid:08x}-{n:08x}")
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Tests
+//////////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::Cursor;
+
+    #[test]
+    fn method_round_trip() {
+        for m in [
+            Method::Get,
+            Method::Head,
+            Method::Post,
+            Method::Put,
+            Method::Delete,
+            Method::Options,
+            Method::Trace,
+            Method::Connect,
+            Method::Patch,
+        ] {
+            assert_eq!(Method::from_bytes(m.as_str().as_bytes()), m);
+        }
+    }
+
+    #[test]
+    fn method_extension() {
+        let m = Method::from_bytes(b"FOO");
+        assert_eq!(m, Method::Other("FOO".into()));
+        assert_eq!(m.as_str(), "FOO");
+    }
+
+    #[test]
+    fn status_classes() {
+        assert!(Status(100).is_informational());
+        assert!(Status(204).is_success());
+        assert!(Status(301).is_redirection());
+        assert!(Status(404).is_client_error());
+        assert!(Status(500).is_server_error());
+    }
+
+    #[test]
+    fn status_reasons() {
+        assert_eq!(Status(200).canonical_reason(), Some("OK"));
+        assert_eq!(Status(404).canonical_reason(), Some("Not Found"));
+        assert_eq!(Status(599).canonical_reason(), None);
+    }
+
+    #[test]
+    fn headers_case_insensitive() {
+        let mut h = Headers::new();
+        h.append("Content-Type", "text/plain");
+        assert_eq!(h.get("content-type"), Some("text/plain"));
+        assert_eq!(h.get("CONTENT-TYPE"), Some("text/plain"));
+    }
+
+    #[test]
+    fn headers_set_replaces() {
+        let mut h = Headers::new();
+        h.append("X", "1");
+        h.append("x", "2");
+        h.set("X", "3");
+        assert_eq!(h.get("X"), Some("3"));
+        assert_eq!(h.len(), 1);
+    }
+
+    #[test]
+    fn headers_multi_value() {
+        let mut h = Headers::new();
+        h.append("Set-Cookie", "a=1");
+        h.append("Set-Cookie", "b=2");
+        let all: Vec<&str> = h.get_all("set-cookie").collect();
+        assert_eq!(all, vec!["a=1", "b=2"]);
+    }
+
+    #[test]
+    fn parse_request_simple() {
+        let input = b"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n";
+        let (r, n) = parse_request(input).unwrap();
+        assert_eq!(r.method, Method::Get);
+        assert_eq!(r.target, "/foo");
+        assert_eq!(r.version, Version::Http11);
+        assert_eq!(r.headers.get("Host"), Some("example.com"));
+        assert_eq!(n, input.len());
+    }
+
+    #[test]
+    fn parse_request_bare_lf() {
+        // RFC 2616 §19.3 robustness: accept bare LF line endings.
+        let input = b"GET / HTTP/1.0\nHost: example.com\n\n";
+        let (r, _) = parse_request(input).unwrap();
+        assert_eq!(r.method, Method::Get);
+        assert_eq!(r.version, Version::Http10);
+    }
+
+    #[test]
+    fn parse_request_extension_method() {
+        let input = b"FOO / HTTP/1.1\r\n\r\n";
+        let (r, _) = parse_request(input).unwrap();
+        assert_eq!(r.method, Method::Other("FOO".into()));
+    }
+
+    #[test]
+    fn parse_response_simple() {
+        let input = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
+        let (h, n) = parse_response_head(input).unwrap();
+        assert_eq!(h.status, Status(200));
+        assert_eq!(h.version, Version::Http11);
+        assert_eq!(h.reason, "OK");
+        assert_eq!(h.headers.get("Content-Length"), Some("5"));
+        assert_eq!(n, input.len());
+    }
+
+    #[test]
+    fn parse_response_multiword_reason() {
+        let input = b"HTTP/1.1 404 Not Found\r\n\r\n";
+        let (h, _) = parse_response_head(input).unwrap();
+        assert_eq!(h.status, Status(404));
+        assert_eq!(h.reason, "Not Found");
+    }
+
+    #[test]
+    fn parse_obs_fold() {
+        // RFC 2616 §2.2 obs-fold: header value continuation.
+        let input =
+            b"GET / HTTP/1.1\r\nX-Long: one\r\n  two\r\n\tthree\r\n\r\n";
+        let (r, _) = parse_request(input).unwrap();
+        assert_eq!(r.headers.get("X-Long"), Some("one two three"));
+    }
+
+    #[test]
+    fn parse_rejects_bad_version() {
+        let input = b"GET / HTTP/9.9\r\n\r\n";
+        assert!(parse_request(input).is_err());
+    }
+
+    #[test]
+    fn parse_rejects_missing_colon() {
+        let input = b"GET / HTTP/1.1\r\nBadHeader\r\n\r\n";
+        assert!(parse_request(input).is_err());
+    }
+
+    #[test]
+    fn body_length_head_is_empty() {
+        let h = Headers::new();
+        let r = response_body_length(&Method::Head, Status(200), &h).unwrap();
+        assert_eq!(r, BodyLength::Empty);
+    }
+
+    #[test]
+    fn body_length_204_is_empty() {
+        let mut h = Headers::new();
+        h.set("Content-Length", "42");
+        let r = response_body_length(&Method::Get, Status(204), &h).unwrap();
+        assert_eq!(r, BodyLength::Empty);
+    }
+
+    #[test]
+    fn body_length_chunked() {
+        let mut h = Headers::new();
+        h.set("Transfer-Encoding", "chunked");
+        let r = response_body_length(&Method::Get, Status(200), &h).unwrap();
+        assert_eq!(r, BodyLength::Chunked);
+    }
+
+    #[test]
+    fn body_length_chunked_wins_over_content_length() {
+        let mut h = Headers::new();
+        h.set("Transfer-Encoding", "chunked");
+        h.set("Content-Length", "10");
+        let r = response_body_length(&Method::Get, Status(200), &h).unwrap();
+        assert_eq!(r, BodyLength::Chunked);
+    }
+
+    #[test]
+    fn body_length_close_delimited_default() {
+        let h = Headers::new();
+        let r = response_body_length(&Method::Get, Status(200), &h).unwrap();
+        assert_eq!(r, BodyLength::CloseDelimited);
+    }
+
+    #[test]
+    fn body_length_request_no_body_default() {
+        let h = Headers::new();
+        let r = request_body_length(&h).unwrap();
+        assert_eq!(r, BodyLength::Empty);
+    }
+
+    #[test]
+    fn chunked_decode() {
+        // RFC 2616 §3.6.1 example.
+        let input =
+            b"4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n";
+        let mut r = ChunkedReader::new(Cursor::new(&input[..]));
+        let mut out = Vec::new();
+        r.read_to_end(&mut out).unwrap();
+        assert_eq!(out, b"Wikipedia in\r\n\r\nchunks.");
+    }
+
+    #[test]
+    fn chunked_decode_with_trailer() {
+        let input = b"5\r\nhello\r\n0\r\nX-Trailer: v\r\n\r\n";
+        let mut r = ChunkedReader::new(Cursor::new(&input[..]));
+        let mut out = Vec::new();
+        r.read_to_end(&mut out).unwrap();
+        assert_eq!(out, b"hello");
+    }
+
+    #[test]
+    fn chunked_decode_with_extension() {
+        let input = b"5;name=foo\r\nhello\r\n0\r\n\r\n";
+        let mut r = ChunkedReader::new(Cursor::new(&input[..]));
+        let mut out = Vec::new();
+        r.read_to_end(&mut out).unwrap();
+        assert_eq!(out, b"hello");
+    }
+
+    #[test]
+    fn chunked_round_trip() {
+        let mut buf = Vec::new();
+        {
+            let mut w = ChunkedWriter::new(&mut buf);
+            w.write_all(b"Hello, ").unwrap();
+            w.write_all(b"world!").unwrap();
+            w.finish().unwrap();
+        }
+        let mut r = ChunkedReader::new(Cursor::new(&buf));
+        let mut out = Vec::new();
+        r.read_to_end(&mut out).unwrap();
+        assert_eq!(out, b"Hello, world!");
+    }
+
+    #[test]
+    fn request_write_head() {
+        let req = Request {
+            method: Method::Post,
+            target: "/x".into(),
+            version: Version::Http11,
+            headers: {
+                let mut h = Headers::new();
+                h.set("Host", "example.com");
+                h
+            },
+        };
+        let mut buf = Vec::new();
+        req.write_head(&mut buf).unwrap();
+        assert_eq!(buf, b"POST /x HTTP/1.1\r\nHost: example.com\r\n\r\n");
+    }
+
+    #[test]
+    fn percent_encode_basic() {
+        assert_eq!(percent_encode("a b+c"), "a%20b%2Bc");
+        assert_eq!(percent_encode("~-._"), "~-._");
+    }
+
+    #[test]
+    fn percent_decode_basic() {
+        assert_eq!(percent_decode("a%20b%2Bc"), "a b+c");
+        assert_eq!(percent_decode("a+b"), "a b");
+    }
+
+    #[test]
+    fn url_form_encoding() {
+        let s = url_form(&[("k1", "v 1"), ("k2", "a&b")]);
+        assert_eq!(s, "k1=v%201&k2=a%26b");
+    }
+
+    #[test]
+    fn parse_url_defaults() {
+        let (s, h, p, path) = parse_url("https://example.com/").unwrap();
+        assert_eq!(s, Scheme::Https);
+        assert_eq!(h, "example.com");
+        assert_eq!(p, 443);
+        assert_eq!(path, "/");
+    }
+
+    #[test]
+    fn parse_url_with_port_and_path() {
+        let (s, h, p, path) =
+            parse_url("http://localhost:8080/api?x=1").unwrap();
+        assert_eq!(s, Scheme::Http);
+        assert_eq!(h, "localhost");
+        assert_eq!(p, 8080);
+        assert_eq!(path, "/api?x=1");
+    }
+
+    #[test]
+    fn parse_url_missing_path() {
+        let (_, _, _, path) = parse_url("http://example.com").unwrap();
+        assert_eq!(path, "/");
+    }
+
+    #[test]
+    fn parse_url_rejects_bad_scheme() {
+        assert!(parse_url("ftp://example.com/").is_err());
+    }
+
+    #[test]
+    fn final_chunked_detection() {
+        assert!(is_final_chunked("chunked"));
+        assert!(is_final_chunked("gzip, chunked"));
+        assert!(is_final_chunked("  chunked  "));
+        assert!(!is_final_chunked("chunked, gzip"));
+        assert!(!is_final_chunked("gzip"));
+    }
+
+    #[test]
+    fn multipart_format() {
+        let form = Multipart::new().text("name", "Murilo").file(
+            "upload",
+            "hello.txt",
+            "text/plain",
+            b"hello".to_vec(),
+        );
+        let boundary = form.boundary().to_owned();
+        let s = String::from_utf8(form.into_bytes()).unwrap();
+        let expected = format!(
+            "--{b}\r\n\
+             Content-Disposition: form-data; name=\"name\"\r\n\
+             \r\n\
+             Murilo\r\n\
+             --{b}\r\n\
+             Content-Disposition: form-data; name=\"upload\"; \
+             filename=\"hello.txt\"\r\n\
+             Content-Type: text/plain\r\n\
+             \r\n\
+             hello\r\n\
+             --{b}--\r\n",
+            b = boundary,
+        );
+        assert_eq!(s, expected);
+    }
+
+    #[test]
+    fn multipart_content_type_header() {
+        let form = Multipart::new();
+        let ct = form.content_type();
+        let prefix = "multipart/form-data; boundary=";
+        assert!(ct.starts_with(prefix));
+        assert_eq!(&ct[prefix.len()..], form.boundary());
+    }
+
+    #[test]
+    fn multipart_quotes_special_chars() {
+        let form = Multipart::new().file(
+            "f",
+            "a\"b\\c.txt",
+            "application/octet-stream",
+            b"x".to_vec(),
+        );
+        let s = String::from_utf8(form.into_bytes()).unwrap();
+        assert!(s.contains("filename=\"a\\\"b\\\\c.txt\""));
+    }
+
+    #[test]
+    fn multipart_unique_boundaries() {
+        let a = Multipart::new();
+        let b = Multipart::new();
+        assert_ne!(a.boundary(), b.boundary());
+    }
+
+    #[test]
+    fn multipart_empty_form() {
+        let form = Multipart::new();
+        let boundary = form.boundary().to_owned();
+        let s = String::from_utf8(form.into_bytes()).unwrap();
+        assert_eq!(s, format!("--{boundary}--\r\n"));
+    }
+}
blob - /dev/null
blob + 89d9655a02f2cd1a90920cd514f0428a57739453 (mode 644)
--- /dev/null
+++ vendor/jackson.rs
@@ -0,0 +1,1171 @@
+// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et :
+//
+// Copyright (c) 2026 Murilo Ijanc' <murilo@ijanc.org>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+//! Parse and generate JSON documents.
+
+use std::fmt;
+use std::fmt::Write as _;
+use std::str::FromStr;
+
+/// A JSON value.
+pub enum Value {
+    Null,
+    Bool(bool),
+    Number(f64),
+    String(String),
+    Array(Vec<Value>),
+    Object(Vec<(String, Value)>),
+}
+
+/// A parse or serialise error.
+pub struct Error {
+    msg: &'static str,
+    pos: usize,
+}
+
+impl Error {
+    pub const fn new(msg: &'static str, pos: usize) -> Self {
+        Self { msg, pos }
+    }
+
+    /// Static error message.
+    pub const fn message(&self) -> &'static str {
+        self.msg
+    }
+
+    /// Byte offset into the input where the error was detected.
+    pub const fn position(&self) -> usize {
+        self.pos
+    }
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} at byte {}", self.msg, self.pos)
+    }
+}
+
+impl fmt::Debug for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
+
+impl std::error::Error for Error {}
+
+impl FromStr for Value {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Parser::new(s).parse_root()
+    }
+}
+
+impl Value {
+    /// Serialise this value as a JSON document.
+    pub fn stringify(&self) -> Result<String, Error> {
+        let mut out = String::new();
+        write_value(self, &mut out)?;
+        Ok(out)
+    }
+
+    pub fn as_bool(&self) -> Option<bool> {
+        match self {
+            Value::Bool(b) => Some(*b),
+            _ => None,
+        }
+    }
+
+    pub fn as_number(&self) -> Option<f64> {
+        match self {
+            Value::Number(n) => Some(*n),
+            _ => None,
+        }
+    }
+
+    pub fn as_str(&self) -> Option<&str> {
+        match self {
+            Value::String(s) => Some(s.as_str()),
+            _ => None,
+        }
+    }
+
+    pub fn as_array(&self) -> Option<&[Value]> {
+        match self {
+            Value::Array(a) => Some(a.as_slice()),
+            _ => None,
+        }
+    }
+
+    pub fn as_object(&self) -> Option<&[(String, Value)]> {
+        match self {
+            Value::Object(o) => Some(o.as_slice()),
+            _ => None,
+        }
+    }
+}
+
+impl From<bool> for Value {
+    fn from(b: bool) -> Self {
+        Value::Bool(b)
+    }
+}
+
+impl From<f64> for Value {
+    fn from(n: f64) -> Self {
+        Value::Number(n)
+    }
+}
+
+impl From<&str> for Value {
+    fn from(s: &str) -> Self {
+        Value::String(s.to_string())
+    }
+}
+
+impl From<String> for Value {
+    fn from(s: String) -> Self {
+        Value::String(s)
+    }
+}
+
+fn write_value(v: &Value, out: &mut String) -> Result<(), Error> {
+    match v {
+        Value::Null => out.push_str("null"),
+        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
+        Value::Number(n) => {
+            if !n.is_finite() {
+                return Err(Error::new("non-finite number", 0));
+            }
+            if *n != 0.0 && n.fract() == 0.0 && n.abs() < (1_i64 << 53) as f64 {
+                write!(out, "{}", *n as i64).unwrap();
+            } else {
+                write!(out, "{n}").unwrap();
+            }
+        }
+        Value::String(s) => write_string(s, out),
+        Value::Array(a) => {
+            out.push('[');
+            for (i, item) in a.iter().enumerate() {
+                if i > 0 {
+                    out.push(',');
+                }
+                write_value(item, out)?;
+            }
+            out.push(']');
+        }
+        Value::Object(o) => {
+            out.push('{');
+            for (i, (k, item)) in o.iter().enumerate() {
+                if i > 0 {
+                    out.push(',');
+                }
+                write_string(k, out);
+                out.push(':');
+                write_value(item, out)?;
+            }
+            out.push('}');
+        }
+    }
+    Ok(())
+}
+
+fn write_string(s: &str, out: &mut String) {
+    out.push('"');
+    let bytes = s.as_bytes();
+    let mut run_start = 0;
+    for (i, &b) in bytes.iter().enumerate() {
+        let esc: &str = match b {
+            b'"' => "\\\"",
+            b'\\' => "\\\\",
+            b'\n' => "\\n",
+            b'\r' => "\\r",
+            b'\t' => "\\t",
+            0x08 => "\\b",
+            0x0C => "\\f",
+            0..=0x1F => {
+                out.push_str(&s[run_start..i]);
+                write!(out, "\\u{:04x}", b).unwrap();
+                run_start = i + 1;
+                continue;
+            }
+            _ => continue,
+        };
+        out.push_str(&s[run_start..i]);
+        out.push_str(esc);
+        run_start = i + 1;
+    }
+    out.push_str(&s[run_start..]);
+    out.push('"');
+}
+
+const MAX_DEPTH: usize = 128;
+
+struct Parser<'a> {
+    src: &'a [u8],
+    pos: usize,
+    depth: usize,
+}
+
+impl<'a> Parser<'a> {
+    fn new(s: &'a str) -> Self {
+        Self {
+            src: s.as_bytes(),
+            pos: 0,
+            depth: 0,
+        }
+    }
+
+    fn err(&self, msg: &'static str) -> Error {
+        Error::new(msg, self.pos)
+    }
+
+    fn peek(&self) -> Option<u8> {
+        self.src.get(self.pos).copied()
+    }
+
+    fn bump(&mut self) -> Option<u8> {
+        let b = self.peek()?;
+        self.pos += 1;
+        Some(b)
+    }
+
+    fn skip_ws(&mut self) {
+        while let Some(b) = self.peek() {
+            if matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
+                self.pos += 1;
+            } else {
+                break;
+            }
+        }
+    }
+
+    fn expect(&mut self, b: u8, msg: &'static str) -> Result<(), Error> {
+        if self.peek() == Some(b) {
+            self.pos += 1;
+            Ok(())
+        } else {
+            Err(self.err(msg))
+        }
+    }
+
+    fn expect_keyword(&mut self, kw: &[u8]) -> Result<(), Error> {
+        let end = self.pos + kw.len();
+        if end > self.src.len() || &self.src[self.pos..end] != kw {
+            return Err(self.err("expected keyword"));
+        }
+        self.pos = end;
+        Ok(())
+    }
+
+    fn enter(&mut self) -> Result<(), Error> {
+        if self.depth >= MAX_DEPTH {
+            return Err(self.err("max nesting depth exceeded"));
+        }
+        self.depth += 1;
+        Ok(())
+    }
+
+    fn parse_root(&mut self) -> Result<Value, Error> {
+        let v = self.parse_value()?;
+        self.skip_ws();
+        if self.pos < self.src.len() {
+            return Err(self.err("trailing garbage"));
+        }
+        Ok(v)
+    }
+
+    fn parse_value(&mut self) -> Result<Value, Error> {
+        self.skip_ws();
+        let b = self
+            .peek()
+            .ok_or_else(|| self.err("unexpected end of input"))?;
+        match b {
+            b'n' => {
+                self.expect_keyword(b"null")?;
+                Ok(Value::Null)
+            }
+            b't' => {
+                self.expect_keyword(b"true")?;
+                Ok(Value::Bool(true))
+            }
+            b'f' => {
+                self.expect_keyword(b"false")?;
+                Ok(Value::Bool(false))
+            }
+            b'"' => Ok(Value::String(self.parse_string()?)),
+            b'[' => self.parse_array(),
+            b'{' => self.parse_object(),
+            b'-' | b'0'..=b'9' => Ok(Value::Number(self.parse_number()?)),
+            _ => Err(self.err("unexpected character")),
+        }
+    }
+
+    fn parse_string(&mut self) -> Result<String, Error> {
+        self.pos += 1;
+        let mut out = String::new();
+        loop {
+            let run_start = self.pos;
+            while let Some(&b) = self.src.get(self.pos) {
+                if matches!(b, b'"' | b'\\') || b < 0x20 {
+                    break;
+                }
+                self.pos += 1;
+            }
+            let run = &self.src[run_start..self.pos];
+            // SAFETY: src is the byte view of a &str input; the scan
+            // only breaks on ASCII bytes ("", \\, < 0x20), so
+            // run_start..self.pos is always a valid UTF-8 substring.
+            let run_str = unsafe { std::str::from_utf8_unchecked(run) };
+            out.push_str(run_str);
+            match self.peek() {
+                None => return Err(self.err("unterminated string")),
+                Some(b'"') => {
+                    self.pos += 1;
+                    return Ok(out);
+                }
+                Some(b'\\') => {
+                    self.pos += 1;
+                    let esc =
+                        self.bump().ok_or_else(|| self.err("bad escape"))?;
+                    match esc {
+                        b'"' => out.push('"'),
+                        b'\\' => out.push('\\'),
+                        b'/' => out.push('/'),
+                        b'b' => out.push('\u{08}'),
+                        b'f' => out.push('\u{0C}'),
+                        b'n' => out.push('\n'),
+                        b'r' => out.push('\r'),
+                        b't' => out.push('\t'),
+                        b'u' => out.push(self.parse_u_escape()?),
+                        _ => return Err(self.err("invalid escape")),
+                    }
+                }
+                Some(_) => {
+                    return Err(self.err("control character in string"));
+                }
+            }
+        }
+    }
+
+    fn parse_u_escape(&mut self) -> Result<char, Error> {
+        let hi = self.parse_hex4()?;
+        if (0xD800..=0xDBFF).contains(&hi) {
+            if self.bump() != Some(b'\\') || self.bump() != Some(b'u') {
+                return Err(self.err("expected low surrogate"));
+            }
+            let lo = self.parse_hex4()?;
+            if !(0xDC00..=0xDFFF).contains(&lo) {
+                return Err(self.err("invalid low surrogate"));
+            }
+            let code = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
+            char::from_u32(code).ok_or_else(|| self.err("invalid codepoint"))
+        } else {
+            char::from_u32(hi).ok_or_else(|| self.err("invalid codepoint"))
+        }
+    }
+
+    fn parse_hex4(&mut self) -> Result<u32, Error> {
+        let mut v: u32 = 0;
+        for _ in 0..4 {
+            let b =
+                self.bump().ok_or_else(|| self.err("bad unicode escape"))?;
+            let d = match b {
+                b'0'..=b'9' => b - b'0',
+                b'a'..=b'f' => b - b'a' + 10,
+                b'A'..=b'F' => b - b'A' + 10,
+                _ => return Err(self.err("bad hex digit")),
+            };
+            v = v * 16 + d as u32;
+        }
+        Ok(v)
+    }
+
+    fn parse_number(&mut self) -> Result<f64, Error> {
+        let start = self.pos;
+        if self.peek() == Some(b'-') {
+            self.pos += 1;
+        }
+        match self.peek() {
+            Some(b'0') => self.pos += 1,
+            Some(b'1'..=b'9') => {
+                self.pos += 1;
+                while matches!(self.peek(), Some(b'0'..=b'9')) {
+                    self.pos += 1;
+                }
+            }
+            _ => return Err(self.err("expected digit")),
+        }
+        if self.peek() == Some(b'.') {
+            self.pos += 1;
+            if !matches!(self.peek(), Some(b'0'..=b'9')) {
+                return Err(self.err("expected digit after decimal point"));
+            }
+            while matches!(self.peek(), Some(b'0'..=b'9')) {
+                self.pos += 1;
+            }
+        }
+        if matches!(self.peek(), Some(b'e' | b'E')) {
+            self.pos += 1;
+            if matches!(self.peek(), Some(b'+' | b'-')) {
+                self.pos += 1;
+            }
+            if !matches!(self.peek(), Some(b'0'..=b'9')) {
+                return Err(self.err("expected digit in exponent"));
+            }
+            while matches!(self.peek(), Some(b'0'..=b'9')) {
+                self.pos += 1;
+            }
+        }
+        let slice = &self.src[start..self.pos];
+        let s = std::str::from_utf8(slice).unwrap();
+        s.parse::<f64>()
+            .map_err(|_| Error::new("invalid number", start))
+    }
+
+    fn parse_array(&mut self) -> Result<Value, Error> {
+        self.pos += 1;
+        self.enter()?;
+        let mut items = Vec::new();
+        self.skip_ws();
+        if self.peek() == Some(b']') {
+            self.pos += 1;
+            self.depth -= 1;
+            return Ok(Value::Array(items));
+        }
+        loop {
+            items.push(self.parse_value()?);
+            self.skip_ws();
+            match self.peek() {
+                Some(b',') => self.pos += 1,
+                Some(b']') => {
+                    self.pos += 1;
+                    self.depth -= 1;
+                    return Ok(Value::Array(items));
+                }
+                _ => return Err(self.err("expected ',' or ']'")),
+            }
+        }
+    }
+
+    fn parse_object(&mut self) -> Result<Value, Error> {
+        self.pos += 1;
+        self.enter()?;
+        let mut items = Vec::new();
+        self.skip_ws();
+        if self.peek() == Some(b'}') {
+            self.pos += 1;
+            self.depth -= 1;
+            return Ok(Value::Object(items));
+        }
+        loop {
+            if self.peek() != Some(b'"') {
+                return Err(self.err("expected string key"));
+            }
+            let key = self.parse_string()?;
+            self.skip_ws();
+            self.expect(b':', "expected ':'")?;
+            let v = self.parse_value()?;
+            items.push((key, v));
+            self.skip_ws();
+            match self.peek() {
+                Some(b',') => {
+                    self.pos += 1;
+                    self.skip_ws();
+                }
+                Some(b'}') => {
+                    self.pos += 1;
+                    self.depth -= 1;
+                    return Ok(Value::Object(items));
+                }
+                _ => return Err(self.err("expected ',' or '}'")),
+            }
+        }
+    }
+}
+
+/// Trait implemented by types that can be written to a [`Value`].
+pub trait ToJson {
+    fn to_json(&self) -> Value;
+}
+
+/// Trait implemented by types that can be read from a [`Value`].
+pub trait FromJson: Sized {
+    fn from_json(v: &Value) -> Result<Self, Error>;
+
+    /// Produce a value when the corresponding field is absent from the
+    /// parent object.  The default rejects; `Option<T>` overrides to
+    /// return `None`.
+    fn missing_field() -> Result<Self, Error> {
+        Err(Error::new("missing field", 0))
+    }
+}
+
+impl ToJson for bool {
+    fn to_json(&self) -> Value {
+        Value::Bool(*self)
+    }
+}
+
+impl FromJson for bool {
+    fn from_json(v: &Value) -> Result<Self, Error> {
+        v.as_bool().ok_or_else(|| Error::new("expected bool", 0))
+    }
+}
+
+impl ToJson for String {
+    fn to_json(&self) -> Value {
+        Value::String(self.clone())
+    }
+}
+
+impl FromJson for String {
+    fn from_json(v: &Value) -> Result<Self, Error> {
+        v.as_str()
+            .map(str::to_string)
+            .ok_or_else(|| Error::new("expected string", 0))
+    }
+}
+
+impl ToJson for f64 {
+    fn to_json(&self) -> Value {
+        Value::Number(*self)
+    }
+}
+
+impl FromJson for f64 {
+    fn from_json(v: &Value) -> Result<Self, Error> {
+        v.as_number()
+            .ok_or_else(|| Error::new("expected number", 0))
+    }
+}
+
+impl ToJson for f32 {
+    fn to_json(&self) -> Value {
+        Value::Number(*self as f64)
+    }
+}
+
+impl FromJson for f32 {
+    fn from_json(v: &Value) -> Result<Self, Error> {
+        v.as_number()
+            .map(|n| n as f32)
+            .ok_or_else(|| Error::new("expected number", 0))
+    }
+}
+
+macro_rules! jackson_int_impls {
+    ($($t:ty),* $(,)?) => { $(
+        impl ToJson for $t {
+            fn to_json(&self) -> Value {
+                Value::Number(*self as f64)
+            }
+        }
+
+        impl FromJson for $t {
+            fn from_json(v: &Value) -> Result<Self, Error> {
+                let n = v
+                    .as_number()
+                    .ok_or_else(|| Error::new("expected number", 0))?;
+                if n.is_finite()
+                    && n.fract() == 0.0
+                    && n >= <$t>::MIN as f64
+                    && n <= <$t>::MAX as f64
+                {
+                    Ok(n as $t)
+                } else {
+                    Err(Error::new("integer out of range", 0))
+                }
+            }
+        }
+    )* };
+}
+
+jackson_int_impls!(i8, i16, i32, i64, u8, u16, u32, u64);
+
+impl<T: ToJson> ToJson for Option<T> {
+    fn to_json(&self) -> Value {
+        match self {
+            Some(v) => v.to_json(),
+            None => Value::Null,
+        }
+    }
+}
+
+impl<T: FromJson> FromJson for Option<T> {
+    fn from_json(v: &Value) -> Result<Self, Error> {
+        match v {
+            Value::Null => Ok(None),
+            _ => T::from_json(v).map(Some),
+        }
+    }
+
+    fn missing_field() -> Result<Self, Error> {
+        Ok(None)
+    }
+}
+
+impl<T: ToJson> ToJson for Vec<T> {
+    fn to_json(&self) -> Value {
+        Value::Array(self.iter().map(T::to_json).collect())
+    }
+}
+
+impl<T: FromJson> FromJson for Vec<T> {
+    fn from_json(v: &Value) -> Result<Self, Error> {
+        let a = v
+            .as_array()
+            .ok_or_else(|| Error::new("expected array", 0))?;
+        a.iter().map(T::from_json).collect()
+    }
+}
+
+/// Define a struct together with [`ToJson`] and [`FromJson`] impls.
+#[macro_export]
+macro_rules! json_struct {
+    (
+        $(#[$meta:meta])*
+        $vis:vis struct $name:ident {
+            $(
+                $(#[$fmeta:meta])*
+                $fvis:vis $field:ident : $ty:ty
+            ),* $(,)?
+        }
+    ) => {
+        $(#[$meta])*
+        $vis struct $name {
+            $(
+                $(#[$fmeta])*
+                $fvis $field: $ty,
+            )*
+        }
+
+        impl $crate::ToJson for $name {
+            fn to_json(&self) -> $crate::Value {
+                $crate::Value::Object(vec![
+                    $(
+                        (
+                            stringify!($field).to_string(),
+                            <$ty as $crate::ToJson>::to_json(&self.$field),
+                        ),
+                    )*
+                ])
+            }
+        }
+
+        impl $crate::FromJson for $name {
+            fn from_json(
+                v: &$crate::Value,
+            ) -> ::std::result::Result<Self, $crate::Error> {
+                let obj = v.as_object().ok_or_else(|| {
+                    $crate::Error::new("expected object", 0)
+                })?;
+                $( let mut $field: Option<&$crate::Value> = None; )*
+                for (k, val) in obj {
+                    match k.as_str() {
+                        $( stringify!($field) => $field = Some(val), )*
+                        _ => {}
+                    }
+                }
+                Ok(Self {
+                    $(
+                        $field: match $field {
+                            Some(val) => {
+                                <$ty as $crate::FromJson>::from_json(val)?
+                            }
+                            None => <$ty as $crate::FromJson>::missing_field()
+                                .map_err(|_| $crate::Error::new(
+                                    concat!(
+                                        "missing field: ",
+                                        stringify!($field),
+                                    ),
+                                    0,
+                                ))?,
+                        },
+                    )*
+                })
+            }
+        }
+    };
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn value_accessors() {
+        assert_eq!(Value::Bool(true).as_bool(), Some(true));
+        assert_eq!(Value::Null.as_bool(), None);
+        assert_eq!(Value::Number(1.5).as_number(), Some(1.5));
+        assert_eq!(Value::Null.as_number(), None);
+        assert_eq!(Value::String("hi".into()).as_str(), Some("hi"));
+        assert_eq!(Value::Null.as_str(), None);
+        assert!(Value::Array(Vec::new()).as_array().is_some());
+        assert!(Value::Null.as_array().is_none());
+        assert!(Value::Object(Vec::new()).as_object().is_some());
+        assert!(Value::Null.as_object().is_none());
+    }
+
+    #[test]
+    fn value_from_impls() {
+        assert!(matches!(Value::from(true), Value::Bool(true)));
+        assert!(matches!(Value::from(1.5), Value::Number(n) if n == 1.5));
+        match Value::from("hello") {
+            Value::String(s) => assert_eq!(s, "hello"),
+            _ => panic!(),
+        }
+        match Value::from(String::from("world")) {
+            Value::String(s) => assert_eq!(s, "world"),
+            _ => panic!(),
+        }
+    }
+
+    #[test]
+    fn error_accessors() {
+        let e = Error::new("boom", 42);
+        assert_eq!(e.message(), "boom");
+        assert_eq!(e.position(), 42);
+    }
+
+    #[test]
+    fn value_variants_construct() {
+        let _ = Value::Null;
+        let _ = Value::Bool(true);
+        let _ = Value::Number(0.0);
+        let _ = Value::String(String::new());
+        let _ = Value::Array(Vec::new());
+        let _ = Value::Object(Vec::new());
+    }
+
+    fn parse(s: &str) -> Value {
+        s.parse::<Value>().unwrap()
+    }
+
+    fn parse_err(s: &str) {
+        assert!(s.parse::<Value>().is_err(), "expected error for {s:?}");
+    }
+
+    #[test]
+    fn parse_primitives() {
+        assert!(matches!(parse("null"), Value::Null));
+        assert!(matches!(parse("true"), Value::Bool(true)));
+        assert!(matches!(parse("false"), Value::Bool(false)));
+        assert!(matches!(parse("  null  "), Value::Null));
+    }
+
+    #[test]
+    fn parse_numbers() {
+        let cases: &[(&str, f64)] = &[
+            ("0", 0.0),
+            ("-0", 0.0),
+            ("42", 42.0),
+            ("-42", -42.0),
+            ("3.14", 3.14),
+            ("-2.5", -2.5),
+            ("1e10", 1e10),
+            ("1E10", 1e10),
+            ("1.5e2", 150.0),
+            ("1.5e+2", 150.0),
+            ("2.5e-1", 0.25),
+        ];
+        for (s, v) in cases {
+            match parse(s) {
+                Value::Number(n) => assert_eq!(n, *v, "{s}"),
+                _ => panic!("not a number: {s}"),
+            }
+        }
+    }
+
+    #[test]
+    fn parse_number_errors() {
+        parse_err("01");
+        parse_err("+1");
+        parse_err(".5");
+        parse_err("1.");
+        parse_err("1e");
+        parse_err("1e+");
+        parse_err("-");
+    }
+
+    #[test]
+    fn parse_strings() {
+        match parse(r#""hello""#) {
+            Value::String(s) => assert_eq!(s, "hello"),
+            _ => panic!(),
+        }
+        match parse(r#""a\"b\\c\/d""#) {
+            Value::String(s) => assert_eq!(s, "a\"b\\c/d"),
+            _ => panic!(),
+        }
+        match parse(r#""\n\r\t\b\f""#) {
+            Value::String(s) => assert_eq!(s, "\n\r\t\u{08}\u{0C}"),
+            _ => panic!(),
+        }
+        match parse(r#""\u0041""#) {
+            Value::String(s) => assert_eq!(s, "A"),
+            _ => panic!(),
+        }
+        match parse(r#""\uD834\uDD1E""#) {
+            Value::String(s) => assert_eq!(s, "\u{1D11E}"),
+            _ => panic!(),
+        }
+        match parse("\"é\"") {
+            Value::String(s) => assert_eq!(s, "é"),
+            _ => panic!(),
+        }
+    }
+
+    #[test]
+    fn parse_string_errors() {
+        parse_err(r#""unterminated"#);
+        parse_err("\"embedded\nnewline\"");
+        parse_err(r#""\x""#);
+        parse_err(r#""\uD800""#);
+        parse_err(r#""\uDC00""#);
+        parse_err(r#""\uD800\u0041""#);
+    }
+
+    #[test]
+    fn parse_arrays() {
+        assert!(matches!(parse("[]"), Value::Array(a) if a.is_empty()));
+        match parse("[1, 2, 3]") {
+            Value::Array(a) => {
+                assert_eq!(a.len(), 3);
+                assert!(matches!(a[0], Value::Number(n) if n == 1.0));
+            }
+            _ => panic!(),
+        }
+        match parse("[[1], [2, 3]]") {
+            Value::Array(a) => assert_eq!(a.len(), 2),
+            _ => panic!(),
+        }
+    }
+
+    #[test]
+    fn parse_array_errors() {
+        parse_err("[");
+        parse_err("[1,");
+        parse_err("[1 2]");
+        parse_err("[,]");
+    }
+
+    #[test]
+    fn parse_objects() {
+        assert!(matches!(parse("{}"), Value::Object(m) if m.is_empty()));
+        match parse(r#"{"a": 1, "b": true}"#) {
+            Value::Object(m) => {
+                assert_eq!(m.len(), 2);
+                assert_eq!(m[0].0, "a");
+                assert!(matches!(m[0].1, Value::Number(n) if n == 1.0));
+                assert_eq!(m[1].0, "b");
+                assert!(matches!(m[1].1, Value::Bool(true)));
+            }
+            _ => panic!(),
+        }
+    }
+
+    #[test]
+    fn parse_object_errors() {
+        parse_err("{");
+        parse_err(r#"{"a""#);
+        parse_err(r#"{"a":}"#);
+        parse_err(r#"{"a":1"#);
+        parse_err(r#"{a:1}"#);
+        parse_err(r#"{"a":1,}"#);
+    }
+
+    #[test]
+    fn parse_duplicate_keys_kept() {
+        match parse(r#"{"a":1,"a":2}"#) {
+            Value::Object(m) => {
+                assert_eq!(m.len(), 2);
+                assert_eq!(m[0].0, "a");
+                assert!(matches!(m[0].1, Value::Number(n) if n == 1.0));
+                assert_eq!(m[1].0, "a");
+                assert!(matches!(m[1].1, Value::Number(n) if n == 2.0));
+            }
+            _ => panic!(),
+        }
+    }
+
+    #[test]
+    fn parse_trailing_garbage_fails() {
+        parse_err("null null");
+        parse_err("1 2");
+        parse_err("[] x");
+    }
+
+    #[test]
+    fn parse_empty_fails() {
+        parse_err("");
+        parse_err("   ");
+    }
+
+    #[test]
+    fn round_trip() {
+        let cases = &[
+            "null",
+            "true",
+            "false",
+            "0",
+            "-1.5",
+            r#""hello""#,
+            "[]",
+            "[1,2,3]",
+            "{}",
+            r#"{"a":1,"b":[true,null]}"#,
+        ];
+        for s in cases {
+            let v: Value = s.parse().unwrap();
+            assert_eq!(&v.stringify().unwrap(), s, "round trip {s}");
+        }
+    }
+
+    #[test]
+    fn parse_rejects_deep_nesting() {
+        let s: String = "[".repeat(200);
+        parse_err(&s);
+    }
+
+    #[test]
+    fn stringify_primitives() {
+        assert_eq!(Value::Null.stringify().unwrap(), "null");
+        assert_eq!(Value::Bool(true).stringify().unwrap(), "true");
+        assert_eq!(Value::Bool(false).stringify().unwrap(), "false");
+        assert_eq!(Value::Number(0.0).stringify().unwrap(), "0");
+        assert_eq!(Value::Number(-42.5).stringify().unwrap(), "-42.5");
+    }
+
+    #[test]
+    fn stringify_integer_values() {
+        assert_eq!(Value::Number(42.0).stringify().unwrap(), "42");
+        assert_eq!(Value::Number(-1000.0).stringify().unwrap(), "-1000");
+        assert_eq!(Value::Number(1.5).stringify().unwrap(), "1.5");
+        // Beyond 2^53 falls back to the f64 formatter.
+        let big = (1_i64 << 54) as f64;
+        let s = Value::Number(big).stringify().unwrap();
+        assert_eq!(s.parse::<f64>().unwrap(), big);
+    }
+
+    #[test]
+    fn stringify_non_finite_errors() {
+        assert!(Value::Number(f64::NAN).stringify().is_err());
+        assert!(Value::Number(f64::INFINITY).stringify().is_err());
+        assert!(Value::Number(f64::NEG_INFINITY).stringify().is_err());
+    }
+
+    #[test]
+    fn stringify_string_escapes() {
+        assert_eq!(Value::String("hi".into()).stringify().unwrap(), "\"hi\"");
+        assert_eq!(
+            Value::String("a\"b\\c".into()).stringify().unwrap(),
+            "\"a\\\"b\\\\c\""
+        );
+        assert_eq!(
+            Value::String("\n\r\t\u{08}\u{0C}".into())
+                .stringify()
+                .unwrap(),
+            "\"\\n\\r\\t\\b\\f\""
+        );
+        assert_eq!(
+            Value::String("\x01\x1f".into()).stringify().unwrap(),
+            "\"\\u0001\\u001f\""
+        );
+        assert_eq!(Value::String("é".into()).stringify().unwrap(), "\"é\"");
+    }
+
+    #[test]
+    fn stringify_array() {
+        assert_eq!(Value::Array(Vec::new()).stringify().unwrap(), "[]");
+        let a = Value::Array(vec![
+            Value::Number(1.0),
+            Value::Null,
+            Value::Bool(true),
+        ]);
+        assert_eq!(a.stringify().unwrap(), "[1,null,true]");
+    }
+
+    #[test]
+    fn stringify_object_preserves_insertion_order() {
+        let o = vec![
+            ("b".into(), Value::Number(2.0)),
+            ("a".into(), Value::Number(1.0)),
+        ];
+        assert_eq!(Value::Object(o).stringify().unwrap(), r#"{"b":2,"a":1}"#);
+    }
+
+    #[test]
+    fn stringify_nested() {
+        let inner = vec![("x".into(), Value::Array(vec![Value::Number(3.0)]))];
+        let outer = vec![("obj".into(), Value::Object(inner))];
+        assert_eq!(
+            Value::Object(outer).stringify().unwrap(),
+            r#"{"obj":{"x":[3]}}"#
+        );
+    }
+
+    #[test]
+    fn stringify_array_propagates_error() {
+        let a = Value::Array(vec![Value::Number(f64::NAN)]);
+        assert!(a.stringify().is_err());
+    }
+
+    json_struct! {
+        struct SimpleUser {
+            name: String,
+            age: i64,
+            active: bool,
+        }
+    }
+
+    #[test]
+    fn derive_simple_round_trip() {
+        let u = SimpleUser {
+            name: "ada".into(),
+            age: 36,
+            active: true,
+        };
+        let s = u.to_json().stringify().unwrap();
+        let v: Value = s.parse().unwrap();
+        let back = SimpleUser::from_json(&v).unwrap();
+        assert_eq!(back.name, "ada");
+        assert_eq!(back.age, 36);
+        assert!(back.active);
+    }
+
+    json_struct! {
+        struct OptionalFields {
+            required: String,
+            maybe: Option<i64>,
+        }
+    }
+
+    #[test]
+    fn derive_option_none_serialises_as_null() {
+        let u = OptionalFields {
+            required: "x".into(),
+            maybe: None,
+        };
+        let s = u.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"required":"x","maybe":null}"#);
+        let back = OptionalFields::from_json(&s.parse().unwrap()).unwrap();
+        assert!(back.maybe.is_none());
+    }
+
+    #[test]
+    fn derive_absent_option_is_none() {
+        let v: Value = r#"{"required":"x"}"#.parse().unwrap();
+        let back = OptionalFields::from_json(&v).unwrap();
+        assert_eq!(back.required, "x");
+        assert!(back.maybe.is_none());
+    }
+
+    #[test]
+    fn derive_option_some_round_trips() {
+        let u = OptionalFields {
+            required: "x".into(),
+            maybe: Some(42),
+        };
+        let s = u.to_json().stringify().unwrap();
+        let back = OptionalFields::from_json(&s.parse().unwrap()).unwrap();
+        assert_eq!(back.maybe, Some(42));
+    }
+
+    json_struct! {
+        struct Container {
+            tags: Vec<String>,
+            counts: Vec<i32>,
+        }
+    }
+
+    #[test]
+    fn derive_vec_round_trip() {
+        let c = Container {
+            tags: vec!["a".into(), "b".into()],
+            counts: vec![1, 2, 3],
+        };
+        let s = c.to_json().stringify().unwrap();
+        let back = Container::from_json(&s.parse().unwrap()).unwrap();
+        assert_eq!(back.tags, vec!["a".to_string(), "b".to_string()]);
+        assert_eq!(back.counts, vec![1, 2, 3]);
+    }
+
+    json_struct! {
+        struct Inner {
+            value: i32,
+        }
+    }
+
+    json_struct! {
+        struct Outer {
+            name: String,
+            inner: Inner,
+        }
+    }
+
+    #[test]
+    fn derive_nested_struct() {
+        let o = Outer {
+            name: "n".into(),
+            inner: Inner { value: 7 },
+        };
+        let s = o.to_json().stringify().unwrap();
+        assert_eq!(s, r#"{"name":"n","inner":{"value":7}}"#);
+        let back = Outer::from_json(&s.parse().unwrap()).unwrap();
+        assert_eq!(back.name, "n");
+        assert_eq!(back.inner.value, 7);
+    }
+
+    #[test]
+    fn derive_missing_field_errors() {
+        let v: Value = r#"{"name":"ada","active":true}"#.parse().unwrap();
+        let e = SimpleUser::from_json(&v).err().unwrap();
+        assert_eq!(e.message(), "missing field: age");
+    }
+
+    #[test]
+    fn derive_wrong_type_errors() {
+        let v: Value =
+            r#"{"name":"ada","age":"old","active":true}"#.parse().unwrap();
+        assert!(SimpleUser::from_json(&v).is_err());
+    }
+
+    #[test]
+    fn derive_expects_object() {
+        let v = Value::Array(Vec::new());
+        assert!(SimpleUser::from_json(&v).is_err());
+    }
+
+    #[test]
+    fn derive_integer_out_of_range_errors() {
+        assert!(i32::from_json(&Value::Number(1e20)).is_err());
+    }
+
+    #[test]
+    fn derive_non_integer_errors() {
+        assert!(i64::from_json(&Value::Number(1.5)).is_err());
+    }
+
+    #[test]
+    fn derive_duplicate_keys_last_wins() {
+        let v: Value =
+            r#"{"name":"a","age":1,"active":true,"age":2}"#.parse().unwrap();
+        let back = SimpleUser::from_json(&v).unwrap();
+        assert_eq!(back.age, 2);
+    }
+}