From c0cfa54ce79847e8c39e860b1c839d4ff865b893 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 28 Aug 2026 03:43:33 -0700 Subject: [PATCH] Serve HEY over MCP with hey mcp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hey mcp runs an MCP server on stdin/stdout: seven domain gateway tools (boxes, search, threads, contacts, todos, calendar, identity) derived from hey-sdk's model exports via the shared toolkit at github.com/basecamp/mcp, dispatching real API calls through the CLI's authenticated, account-scoped SDK client — the same keychain-stored credentials every other command uses. The catalog vendors a hey-sdk model snapshot (behavior-model.json + openapi.json at go/v0.28.0, the version go.mod pins — kept in lockstep by test), synced by scripts/sync-mcp-model.sh with provenance recorded, and the dispatcher maps catalog operations onto the SDK's generic verbs: path and required query params enforced, substituted, and escaped, remaining params gathered into the request body with property names checked against the body schema as a typo guard. Listings with more pages surface their geared_pagination cursor as {"next_page": cursor, "results": ...} — HEY pages by cursor, and a numeric page is answered with the first page forever. Failures are in-band isError results; stdout belongs to the MCP wire, logs go to stderr. --read-only serves only read-only actions and refuses write dispatch; --domains narrows the served surface, failing closed on unknown keys. --- .surface | 3 + Makefile | 7 + README.md | 26 + go.mod | 9 + go.sum | 18 +- internal/cmd/help.go | 2 +- internal/cmd/help_test.go | 1 + internal/cmd/mcp.go | 110 + internal/cmd/mcp_test.go | 222 + internal/cmd/root.go | 1 + internal/cmd/sdk.go | 14 + internal/mcpserver/catalog.go | 37 + internal/mcpserver/catalog_test.go | 104 + internal/mcpserver/dispatch.go | 302 + internal/mcpserver/dispatch_test.go | 347 + internal/mcpserver/domains.go | 51 + internal/mcpserver/model/PROVENANCE.json | 7 + internal/mcpserver/model/behavior-model.json | 1700 ++ internal/mcpserver/model/openapi.json | 14035 +++++++++++++++++ internal/mcpserver/server.go | 67 + internal/mcpserver/server_test.go | 144 + nix/package.nix | 2 +- scripts/sync-mcp-model.sh | 43 + 23 files changed, 17248 insertions(+), 4 deletions(-) create mode 100644 internal/cmd/mcp.go create mode 100644 internal/cmd/mcp_test.go create mode 100644 internal/mcpserver/catalog.go create mode 100644 internal/mcpserver/catalog_test.go create mode 100644 internal/mcpserver/dispatch.go create mode 100644 internal/mcpserver/dispatch_test.go create mode 100644 internal/mcpserver/domains.go create mode 100644 internal/mcpserver/model/PROVENANCE.json create mode 100644 internal/mcpserver/model/behavior-model.json create mode 100644 internal/mcpserver/model/openapi.json create mode 100644 internal/mcpserver/server.go create mode 100644 internal/mcpserver/server_test.go create mode 100755 scripts/sync-mcp-model.sh diff --git a/.surface b/.surface index bf681bab..342a9027 100644 --- a/.surface +++ b/.surface @@ -252,6 +252,9 @@ hey login --cookie hey login --no-browser hey login --token hey logout +hey mcp +hey mcp --domains +hey mcp --read-only hey move hey move --to hey reply diff --git a/Makefile b/Makefile index a1b9841c..8126fa20 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,7 @@ help: @echo "" @echo " make check-surface Verify .surface matches the command tree" @echo " make update-surface Regenerate .surface" + @echo " make update-mcp-model Refresh the vendored hey-sdk model snapshot for hey mcp (SDK=path)" @echo " make check-surface-compat Compare .surface against the previous release tag" @echo " make check-size Check the built binary against .size-budget" @echo " make check-release-lockstep Verify release tool pins and script references agree" @@ -193,6 +194,12 @@ update-surface: check-toolchain @HEY_NO_KEYRING=1 go test ./internal/cmd/ -run TestSurfaceSnapshot -count=1 @echo ".surface updated" +# Refresh the vendored hey-sdk model snapshot the MCP catalog embeds +# (internal/mcpserver/model/). Keep it in lockstep with the hey-sdk version +# pinned in go.mod. SDK=path names a hey-sdk checkout (default ../hey-sdk). +update-mcp-model: + @scripts/sync-mcp-model.sh $(SDK) + # Compare .surface against the previous release tag (removals fail unless # acknowledged in .surface-breaking) check-surface-compat: diff --git a/README.md b/README.md index d179e19a..1b91df05 100644 --- a/README.md +++ b/README.md @@ -750,6 +750,32 @@ hey only ever writes skill directories it owns: each one it creates carries a `hey` skill directory (or symlink) without it — a hand-authored skill at one of those paths is never overwritten or claimed. `hey doctor` flags an unmanaged baseline and how to adopt it. +### MCP server + +`hey mcp` runs an MCP (Model Context Protocol) server on stdin/stdout, serving HEY +boxes, search, threads, contacts, todos, calendars, and your identity as tools +backed by your signed-in account — the same keychain-stored credentials every other command uses. +Register it with any MCP client as a stdio server: + +```bash +claude mcp add hey -- hey mcp # Claude Code +hey mcp --read-only # serve only read-only actions +hey mcp --domains boxes,search # narrow the served tool surface +``` + +Each domain is one gateway tool (`hey_boxes`, `hey_search`, `hey_threads`, +`hey_contacts`, `hey_todos`, `hey_calendar`, `hey_identity`) dispatching actions +derived from the HEY SDK's API model; call an action named `describe` for any +action's parameter schema. Listings with more pages come back as +`{"next_page": cursor, "results": ...}` — pass the cursor back as the action's +`page` parameter. The posting-changes feed's last page comes back as +`{"next_since": ..., "next_v": ..., "results": ...}` — the cursor for the next +incremental poll, passed back as the action's `since` and `v` parameters. +Mutations are never retried automatically: a 429/503 on a write surfaces to +the caller rather than risking a duplicate delivery, so retry a failed write +yourself once you know it did not land. Logs go to stderr — stdout carries +the MCP wire protocol. + ## Troubleshooting ```bash diff --git a/go.mod b/go.mod index a51269c6..c4543c11 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,13 @@ require ( charm.land/lipgloss/v2 v2.0.6 github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 github.com/basecamp/hey-sdk/go v0.28.0 + github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d github.com/charmbracelet/x/ansi v0.11.8 github.com/fsnotify/fsnotify v1.10.1 github.com/gofrs/flock v0.13.0 github.com/itchyny/gojq v0.12.19 github.com/mattn/go-runewidth v0.0.28 + github.com/modelcontextprotocol/go-sdk v1.7.0 github.com/sigstore/sigstore-go v1.3.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -77,6 +79,7 @@ require ( github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/certificate-transparency-go v1.3.3 // indirect github.com/google/go-containerregistry v0.21.7 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect @@ -93,16 +96,20 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.1 // indirect github.com/sigstore/rekor v1.5.3 // indirect github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect github.com/sigstore/sigstore v1.10.8 // indirect github.com/sigstore/timestamp-authority/v2 v2.1.3 // indirect + github.com/stretchr/testify v1.12.1 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect github.com/transparency-dev/formats v0.1.1 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -111,7 +118,9 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/text v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect google.golang.org/grpc v1.82.1 // indirect diff --git a/go.sum b/go.sum index 74b1ab42..fff7b424 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655 h1:zz0WUSE github.com/basecamp/actioncable-go v0.0.0-20260824145920-822e6cf08655/go.mod h1:ezaV5z1GXQAsqyejqTs6wCFl2D8Wj+COLQkHc/kwoRs= github.com/basecamp/hey-sdk/go v0.28.0 h1:N3sNaELGngFuEW9cAWner+mwhWH734Fjru/PcTdLIUg= github.com/basecamp/hey-sdk/go v0.28.0/go.mod h1:k6sO2XhMkU3UY8lD2ozp0735Ic3q8xoMQt7YUT3TlYk= +github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4= +github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -220,6 +222,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro= @@ -289,6 +293,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= +github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= @@ -318,6 +324,10 @@ github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgm github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= @@ -349,8 +359,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= @@ -374,6 +384,8 @@ github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= @@ -431,6 +443,8 @@ golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik= diff --git a/internal/cmd/help.go b/internal/cmd/help.go index a220335e..bcf6bf29 100644 --- a/internal/cmd/help.go +++ b/internal/cmd/help.go @@ -41,7 +41,7 @@ var curatedCategories = []struct { }, { heading: "ACCOUNT & SYSTEM", - names: []string{"auth", "account", "config", "setup", "shell-completion", "doctor", "upgrade", "version"}, + names: []string{"auth", "account", "config", "setup", "mcp", "shell-completion", "doctor", "upgrade", "version"}, }, } diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go index 62415e90..a4f47d0f 100644 --- a/internal/cmd/help_test.go +++ b/internal/cmd/help_test.go @@ -155,6 +155,7 @@ ACCOUNT & SYSTEM account List and select linked mail accounts config View and change settings setup Set up HEY for first use + mcp Serve HEY to MCP clients over stdio shell-completion Set up tab completion for your shell doctor Find login and configuration problems upgrade Upgrade hey to the latest release diff --git a/internal/cmd/mcp.go b/internal/cmd/mcp.go new file mode 100644 index 00000000..a0c952e9 --- /dev/null +++ b/internal/cmd/mcp.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "context" + "log/slog" + "os" + "os/signal" + "syscall" + + hey "github.com/basecamp/hey-sdk/go/pkg/hey" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/spf13/cobra" + + "github.com/basecamp/hey-cli/internal/mcpserver" +) + +// mcpTransport is a seam so tests can drive the server over in-memory +// transports instead of the process's stdin/stdout. +var mcpTransport = func() mcp.Transport { return &mcp.StdioTransport{} } + +type mcpCommand struct { + cmd *cobra.Command + readOnly bool + domains []string +} + +func newMCPCommand() *mcpCommand { + mcpCommand := &mcpCommand{} + mcpCommand.cmd = &cobra.Command{ + Use: "mcp", + Short: "Serve HEY to MCP clients over stdio", + Long: "Run an MCP (Model Context Protocol) server on stdin/stdout, serving HEY mail,\n" + + "contacts, and todos as tools backed by your signed-in account.\n\n" + + "Register it with an MCP client as a stdio server, e.g.:\n\n" + + " claude mcp add hey -- hey mcp", + Args: cobra.NoArgs, + Annotations: map[string]string{ + "agent_notes": "Long-running server; stdout speaks the MCP wire protocol. Not for interactive use.", + }, + RunE: mcpCommand.run, + } + + mcpCommand.cmd.Flags().BoolVar(&mcpCommand.readOnly, "read-only", false, "Serve only read-only actions") + mcpCommand.cmd.Flags().StringSliceVar(&mcpCommand.domains, "domains", nil, "Narrow to specific domains (comma-separated; default all)") + + return mcpCommand +} + +// mcpAPI is the dispatcher's view of the SDK, split by verb: reads ride the +// shared client's retry policy, while mutations go through a twin that never +// retries on 429/503. UpdateMessage delivers mail on PUT, and its contract +// forbids a transparent retry after an ambiguous first attempt — a duplicate +// send is irreversible. POST and PATCH never auto-retried, so sending PUT and +// DELETE through the no-retry twin makes every mutation single-shot. A 401 +// still refreshes the token before the error surfaces, so a caller's own +// retry goes out with fresh credentials. +type mcpAPI struct { + reads, writes *hey.Client +} + +func (a mcpAPI) Get(ctx context.Context, path string) (*hey.Response, error) { + return a.reads.Get(ctx, path) +} + +func (a mcpAPI) Post(ctx context.Context, path string, body any) (*hey.Response, error) { + return a.writes.Post(ctx, path, body) +} + +func (a mcpAPI) Put(ctx context.Context, path string, body any) (*hey.Response, error) { + return a.writes.Put(ctx, path, body) +} + +func (a mcpAPI) Patch(ctx context.Context, path string, body any) (*hey.Response, error) { + return a.writes.Patch(ctx, path, body) +} + +func (a mcpAPI) Delete(ctx context.Context, path string) (*hey.Response, error) { + return a.writes.Delete(ctx, path) +} + +func (c *mcpCommand) run(cmd *cobra.Command, args []string) error { + if err := requireAuth(); err != nil { + return err + } + + // The same account scoping the shared client got in the root command's + // pre-run, applied to the no-retry twin. + writes, err := clientForAccountSelection(cmd.Context(), newSDKClient(hey.WithMaxRetries(0)), cfg.AccountID) + if err != nil { + return err + } + + srv, err := mcpserver.New(mcpAPI{reads: sdk, writes: writes}, mcpserver.Config{ReadOnly: c.readOnly, Domains: c.domains}) + if err != nil { + return err + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + + // Log to stderr: stdout belongs to the MCP wire. + logger := slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), nil)) + session, err := srv.BuildMCPServer(logger).Connect(ctx, mcpTransport(), nil) + if err != nil { + return err + } + logger.Info("MCP server running on stdio", "tools", len(srv.Domains()), "read_only", c.readOnly) + + return session.Wait() +} diff --git a/internal/cmd/mcp_test.go b/internal/cmd/mcp_test.go new file mode 100644 index 00000000..9e564b8a --- /dev/null +++ b/internal/cmd/mcp_test.go @@ -0,0 +1,222 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestMCPCommandRegistration(t *testing.T) { + root := newRootCmd() + command, _, err := root.Find([]string{"mcp"}) + if err != nil || command.Name() != "mcp" { + t.Fatalf("mcp command not registered: %v", err) + } + + readOnly := command.Flags().Lookup("read-only") + if readOnly == nil || readOnly.DefValue != "false" { + t.Errorf("read-only flag = %#v", readOnly) + } + domains := command.Flags().Lookup("domains") + if domains == nil { + t.Error("domains flag missing") + } + + if !commandUsesAccountScope(command) { + t.Error("mcp must use the configured account scope") + } +} + +func TestMCPCommandRequiresAuth(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tmpDir) + t.Setenv("XDG_STATE_HOME", tmpDir) + t.Setenv("XDG_CACHE_HOME", tmpDir) + t.Setenv("HEY_TOKEN", "") + t.Setenv("HEY_NO_KEYRING", "1") + stubInteractive(t, false) + + root := newRootCmd() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetErr(&buf) + root.SetArgs([]string{"mcp"}) + + if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "Not logged in") { + t.Fatalf("err = %v, want auth error", err) + } +} + +// stubMCPTransport swaps the stdio transport for the server side of an +// in-memory pipe and returns the client side. +func stubMCPTransport(t *testing.T) mcp.Transport { + t.Helper() + clientTransport, serverTransport := mcp.NewInMemoryTransports() + orig := mcpTransport + mcpTransport = func() mcp.Transport { return serverTransport } + t.Cleanup(func() { mcpTransport = orig }) + return clientTransport +} + +// runMCPCommand runs `hey mcp` against a stub HEY server and connects a real +// MCP client to it over the transport seam. The command exits when the +// client session closes. +func runMCPCommand(t *testing.T, upstream *httptest.Server, args ...string) *mcp.ClientSession { + t.Helper() + t.Setenv("HEY_TOKEN", "test-token") + t.Setenv("HEY_NO_KEYRING", "1") + tmpDir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tmpDir) + t.Setenv("XDG_STATE_HOME", tmpDir) + t.Setenv("XDG_CACHE_HOME", tmpDir) + + clientTransport := stubMCPTransport(t) + + root := newRootCmd() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetErr(&buf) + root.SetArgs(append([]string{"mcp", "--base-url", upstream.URL}, args...)) + + done := make(chan error, 1) + go func() { done <- root.Execute() }() + t.Cleanup(func() { + if err := <-done; err != nil { + t.Errorf("hey mcp exited with error: %v\n%s", err, buf.String()) + } + }) + + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "0.0.0"}, nil) + session, err := client.Connect(context.Background(), clientTransport, nil) + if err != nil { + t.Fatalf("MCP initialize failed: %v", err) + } + t.Cleanup(func() { _ = session.Close() }) + return session +} + +func TestMCPCommandServesMCP(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/boxes.json" { + t.Errorf("unexpected HTTP request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + return + } + // Tool calls must ride on the CLI's own credentials. + if got := r.Header.Get("Authorization"); got != "Bearer test-token" { + t.Errorf("Authorization = %q, want the CLI's token", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[{"id":1,"name":"Imbox"}]`)) + })) + t.Cleanup(upstream.Close) + + session := runMCPCommand(t, upstream) + + if got := session.InitializeResult().ServerInfo.Name; got != "hey-cli" { + t.Errorf("server name = %q, want hey-cli", got) + } + + var names []string + for tool, err := range session.Tools(context.Background(), nil) { + if err != nil { + t.Fatal(err) + } + names = append(names, tool.Name) + } + if len(names) != 7 { + t.Fatalf("tools = %v, want 7 hey_* tools", names) + } + + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "hey_boxes", + Arguments: map[string]any{"action": "list_boxes", "params": map[string]any{}}, + }) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("list_boxes failed: %v", result.Content) + } + text, ok := result.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("content = %T", result.Content[0]) + } + var boxes []struct { + Name string `json:"name"` + } + if err := json.Unmarshal([]byte(text.Text), &boxes); err != nil || len(boxes) == 0 || boxes[0].Name != "Imbox" { + t.Fatalf("list_boxes result = %q (%v)", text.Text, err) + } +} + +func TestMCPCommandFlagPassthrough(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("unexpected HTTP request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + })) + t.Cleanup(upstream.Close) + + session := runMCPCommand(t, upstream, "--read-only", "--domains", "boxes") + + var tools []*mcp.Tool + for tool, err := range session.Tools(context.Background(), nil) { + if err != nil { + t.Fatal(err) + } + tools = append(tools, tool) + } + if len(tools) != 1 || tools[0].Name != "hey_boxes" { + t.Fatalf("tools = %v, want just hey_boxes", tools) + } + if !strings.Contains(tools[0].Description, "list_boxes") { + t.Error("read-only hey_boxes lost its read actions") + } + if strings.Contains(tools[0].Description, "create_box_designation") { + t.Error("read-only hey_boxes still lists a write action") + } +} + +func TestMCPCommandDoesNotRetryMutations(t *testing.T) { + var deliveries atomic.Int32 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut || !strings.HasPrefix(r.URL.Path, "/messages/1") { + t.Errorf("unexpected HTTP request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + return + } + deliveries.Add(1) + // An ambiguous failure on a delivery: a retry could send the mail + // twice, so the client must surface it instead. + w.Header().Set("Retry-After", "0") + w.WriteHeader(http.StatusTooManyRequests) + })) + t.Cleanup(upstream.Close) + + session := runMCPCommand(t, upstream) + + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ + Name: "hey_threads", + Arguments: map[string]any{"action": "update_message", "params": map[string]any{ + "messageId": "1", + "acting_sender_id": 7, + "message": map[string]any{"subject": "s", "content": "c"}, + }}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatalf("update_message against a 429 upstream did not surface an error: %v", result.Content) + } + if got := deliveries.Load(); got != 1 { + t.Errorf("upstream saw %d delivery attempts, want exactly 1 — a delivery must never be retried", got) + } +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index a6b2e245..06182148 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -221,6 +221,7 @@ func newRootCmd() *cobra.Command { root.AddCommand(newTuiCommand().cmd) root.AddCommand(newHeyCommand().cmd) root.AddCommand(newSkillCommand().cmd) + root.AddCommand(newMCPCommand().cmd) root.AddCommand(newHelpTopicCommands()...) root.AddCommand(newCommandsCommand()) root.AddCommand(newShellCompletionCommand()) diff --git a/internal/cmd/sdk.go b/internal/cmd/sdk.go index ec4ed8e5..91e862cf 100644 --- a/internal/cmd/sdk.go +++ b/internal/cmd/sdk.go @@ -25,6 +25,11 @@ import ( var ( rootSDK *hey.Client sdk *hey.Client + + // The configuration rootSDK was built from, kept so newSDKClient can + // build siblings that share auth, user agent, hooks, and logging. + sdkClientCfg *hey.Config + sdkClientOpts []hey.ClientOption ) // cliAuthStrategy bridges the CLI's auth.Manager to the SDK's AuthStrategy interface. @@ -86,10 +91,19 @@ func initSDK(authMgr *auth.Manager, baseURL string) { sdkStats = &statsHooks{} opts = append(opts, hey.WithHooks(sdkStats)) + sdkClientCfg = sdkCfg + sdkClientOpts = opts rootSDK = hey.NewClient(sdkCfg, nil, opts...) sdk = rootSDK } +// newSDKClient builds another client sharing the CLI's configuration — auth, +// user agent, hooks, logging — plus any extra options. Valid after initSDK. +func newSDKClient(extra ...hey.ClientOption) *hey.Client { + opts := append(append([]hey.ClientOption{}, sdkClientOpts...), extra...) + return hey.NewClient(sdkClientCfg, nil, opts...) +} + func selectConfiguredAccount(ctx context.Context) error { client, err := clientForAccountSelection(ctx, rootSDK, cfg.AccountID) if err != nil { diff --git a/internal/mcpserver/catalog.go b/internal/mcpserver/catalog.go new file mode 100644 index 00000000..49dd4ebc --- /dev/null +++ b/internal/mcpserver/catalog.go @@ -0,0 +1,37 @@ +// Package mcpserver assembles the MCP server behind `hey mcp`: hey's tool +// catalog derived from hey-sdk's model exports, dispatched through the CLI's +// authenticated SDK client. +// +// The generic machinery — joining behavior-model.json with openapi.json, +// rendering domain gateway tools, action dispatch, read-only filtering, the +// in-band describe action — lives in the shared toolkit at +// github.com/basecamp/mcp. This package supplies the product half: the +// curated DomainSpecs mapping hey-sdk tags to domains, the vendored model +// snapshot under model/ (synced by scripts/sync-mcp-model.sh, provenance +// recorded), and the dispatcher that turns catalog operations into hey-sdk +// requests. +package mcpserver + +import ( + "embed" + "fmt" + "io/fs" + + "github.com/basecamp/mcp/catalog" +) + +//go:embed model/behavior-model.json model/openapi.json +var modelFS embed.FS + +// loadCatalog derives hey's catalog from the embedded model snapshot. +func loadCatalog() (*catalog.Catalog, error) { + model, err := fs.Sub(modelFS, "model") + if err != nil { + return nil, fmt.Errorf("embedded model: %w", err) + } + return catalog.Load(catalog.Spec{ + ToolPrefix: "hey_", + Domains: DomainSpecs, + Model: model, + }) +} diff --git a/internal/mcpserver/catalog_test.go b/internal/mcpserver/catalog_test.go new file mode 100644 index 00000000..4f3c7c77 --- /dev/null +++ b/internal/mcpserver/catalog_test.go @@ -0,0 +1,104 @@ +package mcpserver + +import ( + "encoding/json" + "os" + "regexp" + "sort" + "testing" + + "github.com/basecamp/mcp/catalog" +) + +func loadForTest(t *testing.T) *catalog.Catalog { + t.Helper() + cat, err := loadCatalog() + if err != nil { + t.Fatalf("catalog must derive cleanly from the vendored model: %v", err) + } + return cat +} + +func TestCatalogServesCuratedDomains(t *testing.T) { + cat := loadForTest(t) + + tools := make([]string, 0, len(cat.Domains)) + for _, d := range cat.Domains { + tools = append(tools, d.Tool) + if len(d.Operations) == 0 { + t.Errorf("domain %q has no operations", d.Key) + } + } + want := []string{"hey_boxes", "hey_search", "hey_threads", "hey_contacts", "hey_todos", "hey_calendar", "hey_identity"} + if len(tools) != len(want) { + t.Fatalf("tools = %v, want %v", tools, want) + } + for i := range want { + if tools[i] != want[i] { + t.Fatalf("tools = %v, want %v", tools, want) + } + } +} + +// TestCatalogUnmappedTagsArePinned fails when the SDK grows a tag nobody has +// decided about: adding a tag here (or mapping it in DomainSpecs) is the +// deliberate act. +func TestCatalogUnmappedTagsArePinned(t *testing.T) { + cat := loadForTest(t) + + unmapped := make([]string, 0, len(cat.Unmapped)) + for tag := range cat.Unmapped { + unmapped = append(unmapped, tag) + } + sort.Strings(unmapped) + + want := []string{ + "Attachments", "Bulk Reply", "Calendar Habits", "Calendar Journal", + "Calendar Periods", "Calendar Time Tracks", "Clips", + "Collections", "Folders", "Postings", + "Publications", "Snippets", "Stickies", "Workflows", + } + got, _ := json.Marshal(unmapped) + expected, _ := json.Marshal(want) + if string(got) != string(expected) { + t.Fatalf("unmapped tags = %s, want %s", got, expected) + } +} + +// TestCatalogModelProvenance keeps the vendored snapshot in lockstep with +// the hey-sdk release the CLI builds against: a go.mod bump without a +// snapshot refresh (or vice versa) fails here, so MCP never advertises +// routes from a different SDK version than the one linked in. +func TestCatalogModelProvenance(t *testing.T) { + data, err := os.ReadFile("model/PROVENANCE.json") + if err != nil { + t.Fatalf("PROVENANCE.json: %v", err) + } + var provenance struct { + Source string `json:"source"` + Commit string `json:"commit"` + Ref string `json:"ref"` + Files []string `json:"files"` + } + if err := json.Unmarshal(data, &provenance); err != nil { + t.Fatalf("PROVENANCE.json: %v", err) + } + if provenance.Source != "github.com/basecamp/hey-sdk" { + t.Errorf("provenance source = %q", provenance.Source) + } + if provenance.Commit == "" { + t.Error("provenance commit is empty") + } + + gomod, err := os.ReadFile("../../go.mod") + if err != nil { + t.Fatalf("go.mod: %v", err) + } + match := regexp.MustCompile(`github\.com/basecamp/hey-sdk/go (v\S+)`).FindSubmatch(gomod) + if match == nil { + t.Fatal("hey-sdk dependency not found in go.mod") + } + if want := "go/" + string(match[1]); provenance.Ref != want { + t.Errorf("provenance ref = %q, want %q (the hey-sdk version go.mod pins) — run scripts/sync-mcp-model.sh against that checkout", provenance.Ref, want) + } +} diff --git a/internal/mcpserver/dispatch.go b/internal/mcpserver/dispatch.go new file mode 100644 index 00000000..65717925 --- /dev/null +++ b/internal/mcpserver/dispatch.go @@ -0,0 +1,302 @@ +package mcpserver + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + hey "github.com/basecamp/hey-sdk/go/pkg/hey" + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/basecamp/mcp/catalog" + "github.com/basecamp/mcp/gateway" +) + +// API is the slice of the hey-sdk client the dispatcher drives. *hey.Client +// satisfies it; the client carries auth, account scoping, retry, and base +// URL resolution, so the dispatcher only assembles paths and bodies. Retry +// policy is the implementation's to choose per verb — the CLI wires reads +// through a retrying client and every mutation through one that never +// retries, because a retried PUT can deliver a message twice. +type API interface { + Get(ctx context.Context, path string) (*hey.Response, error) + Post(ctx context.Context, path string, body any) (*hey.Response, error) + Put(ctx context.Context, path string, body any) (*hey.Response, error) + Patch(ctx context.Context, path string, body any) (*hey.Response, error) + Delete(ctx context.Context, path string) (*hey.Response, error) +} + +// The CLI hands its *hey.Client straight to New. +var _ API = (*hey.Client)(nil) + +// dispatcher turns catalog operations into hey-sdk requests. +// +// Calling convention: the tool call's params object carries the operation's +// path and query parameters by name, and every remaining entry becomes a +// request body property. The describe action serves the schema for all +// three. Failures are in-band isError results per MCP convention. +type dispatcher struct { + api API +} + +func (d dispatcher) handle(ctx context.Context, dom gateway.Domain, op gateway.Operation, params map[string]any) (*mcp.CallToolResult, error) { + domain, ok := dom.(*catalog.Domain) + if !ok { + return gateway.ErrorResult("internal error: domain %q is not a catalog domain", dom.Name()), nil + } + full, ok := domain.Operation(op.Action) + if !ok { + return gateway.ErrorResult("internal error: action %q not in domain %q", op.Action, dom.Name()), nil + } + + path, body, err := buildRequest(full, params) + if err != nil { + return gateway.ErrorResult("%v", err), nil + } + + resp, err := d.call(ctx, full.Method, path, body) + if err != nil { + return gateway.ErrorResult("%v", err), nil + } + if len(bytes.TrimSpace(resp.Data)) == 0 { + result := map[string]any{"status": resp.StatusCode} + // A draft save answers 204 with the saved entry's path in Location; + // without it the caller would have no way to address what it just + // created. + if location := resp.Headers.Get("Location"); location != "" { + result["location"] = location + } + return gateway.JSONResult(result) + } + if fields := nextCursorFields(resp.Headers, resp.Data); len(fields) > 0 { + result := make(map[string]any, len(fields)+1) + for name, value := range fields { + result[name] = value + } + result["results"] = resp.Data + if wrapped, err := json.Marshal(result); err == nil { + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(wrapped)}}}, nil + } + } + return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: string(resp.Data)}}}, nil +} + +// nextCursorFields extracts the next-read cursor from a geared_pagination Link +// header, falling back to a next_history_url in the response body (how box +// reads carry theirs). HEY pages by cursor, not number — a numeric page is +// answered with the first page forever — so when a listing has more, the +// result is wrapped as {"next_page": cursor, "results": ...} and the caller +// passes the cursor back as the action's page parameter. The changes feed's +// last page names the next incremental poll instead: its Link URL carries +// since (and v) rather than page, surfaced as {"next_since": ..., "next_v": ...} +// for the caller to pass back as the action's since and v parameters. +func nextCursorFields(headers http.Header, data []byte) map[string]string { + for _, link := range headers.Values("Link") { + for part := range strings.SplitSeq(link, ",") { + if !strings.Contains(part, `rel="next"`) { + continue + } + start := strings.Index(part, "<") + end := strings.Index(part, ">") + if start < 0 || end <= start+1 { + continue + } + if fields := linkCursorFields(part[start+1 : end]); len(fields) > 0 { + return fields + } + } + } + if cursor := bodyNextCursor(data); cursor != "" { + return map[string]string{"next_page": cursor} + } + return nil +} + +// linkCursorFields reads the cursor out of one rel="next" URL: a page cursor +// while the read has more pages now, or — on the changes feed's last page — +// the since (and v) where the next incremental poll resumes. +func linkCursorFields(rawURL string) map[string]string { + u, err := url.Parse(rawURL) + if err != nil { + return nil + } + query := u.Query() + if page := query.Get("page"); page != "" { + return map[string]string{"next_page": page} + } + if since := query.Get("since"); since != "" { + fields := map[string]string{"next_since": since} + if v := query.Get("v"); v != "" { + fields["next_v"] = v + } + return fields + } + return nil +} + +// bodyNextCursor finds a next_history_url at the top level of the response — +// or one envelope down, for the nested wire variant box reads may use — and +// returns its page cursor. +func bodyNextCursor(data []byte) string { + var body map[string]json.RawMessage + if err := json.Unmarshal(data, &body); err != nil { + return "" + } + if raw, ok := body["next_history_url"]; ok { + return pageParamJSON(raw) + } + if len(body) == 1 { + for _, raw := range body { + var nested map[string]json.RawMessage + if err := json.Unmarshal(raw, &nested); err == nil { + if inner, ok := nested["next_history_url"]; ok { + return pageParamJSON(inner) + } + } + } + } + return "" +} + +func pageParamJSON(raw json.RawMessage) string { + var next string + if err := json.Unmarshal(raw, &next); err != nil { + return "" + } + return pageParam(next) +} + +// pageParam returns the page query parameter of a pagination URL. +func pageParam(rawURL string) string { + u, err := url.Parse(rawURL) + if err != nil { + return "" + } + return u.Query().Get("page") +} + +func (d dispatcher) call(ctx context.Context, method, path string, body any) (*hey.Response, error) { + switch method { + case "GET": + return d.api.Get(ctx, path) + case "POST": + return d.api.Post(ctx, path, body) + case "PUT": + return d.api.Put(ctx, path, body) + case "PATCH": + return d.api.Patch(ctx, path, body) + case "DELETE": + // No catalog DELETE takes a body; buildRequest already rejected + // stray params for body-less operations. + return d.api.Delete(ctx, path) + default: + return nil, fmt.Errorf("internal error: unsupported method %s", method) + } +} + +// buildRequest resolves the operation's path template and query string from +// params and gathers the remaining entries into the request body. Missing +// path parameters, stray parameters, and non-scalar path or query values are +// errors pointing at the describe action. +func buildRequest(op *catalog.Operation, params map[string]any) (string, any, error) { + consumed := map[string]bool{} + + path := op.Path + for _, p := range op.Params { + if p.In != "path" { + continue + } + raw, ok := params[p.Name] + if !ok { + return "", nil, fmt.Errorf("missing required path parameter %q for action %q (describe the action for its schema)", p.Name, op.Action) + } + value, err := scalarString(raw) + if err != nil { + return "", nil, fmt.Errorf("path parameter %q: %w", p.Name, err) + } + path = strings.ReplaceAll(path, "{"+p.Name+"}", url.PathEscape(value)) + consumed[p.Name] = true + } + + query := url.Values{} + for _, p := range op.Params { + if p.In != "query" { + continue + } + raw, ok := params[p.Name] + if !ok { + if p.Required { + return "", nil, fmt.Errorf("missing required query parameter %q for action %q (describe the action for its schema)", p.Name, op.Action) + } + continue + } + value, err := scalarString(raw) + if err != nil { + return "", nil, fmt.Errorf("query parameter %q: %w", p.Name, err) + } + query.Set(p.Name, value) + consumed[p.Name] = true + } + + body := map[string]any{} + for name, value := range params { + if consumed[name] { + continue + } + if op.Body == nil { + return "", nil, fmt.Errorf("unknown parameter %q for action %q (describe the action for its schema)", name, op.Action) + } + if !bodyAllows(op, name) { + return "", nil, fmt.Errorf("unknown parameter %q for action %q (describe the action for its body schema)", name, op.Action) + } + body[name] = value + } + + if len(query) > 0 { + path += "?" + query.Encode() + } + if len(body) == 0 && (op.Body == nil || !op.BodyRequired) { + return path, nil, nil + } + return path, body, nil +} + +// bodyAllows reports whether the operation's body schema accepts a property +// named name. A schema without declared properties passes everything +// through; otherwise unknown names are rejected unless additionalProperties +// allows them. This is a typo guard, not schema validation: types, required +// properties, and nested constraints are the server's to enforce, and its +// errors come back in-band. +func bodyAllows(op *catalog.Operation, name string) bool { + properties, ok := op.Body["properties"].(map[string]any) + if !ok { + return true + } + if _, ok := properties[name]; ok { + return true + } + if extra, present := op.Body["additionalProperties"]; present { + allowed, isBool := extra.(bool) + return !isBool || allowed + } + return false +} + +// scalarString renders a JSON-decoded path or query value for the wire. +func scalarString(value any) (string, error) { + switch v := value.(type) { + case string: + return v, nil + case bool: + return strconv.FormatBool(v), nil + case float64: + return strconv.FormatFloat(v, 'f', -1, 64), nil + default: + return "", fmt.Errorf("must be a string, number, or boolean, got %T", value) + } +} diff --git a/internal/mcpserver/dispatch_test.go b/internal/mcpserver/dispatch_test.go new file mode 100644 index 00000000..28bfb643 --- /dev/null +++ b/internal/mcpserver/dispatch_test.go @@ -0,0 +1,347 @@ +package mcpserver + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "reflect" + "strings" + "testing" + + hey "github.com/basecamp/hey-sdk/go/pkg/hey" + + "github.com/basecamp/mcp/catalog" +) + +// fakeAPI records the single request the dispatcher makes and answers with a +// canned response or error. +type fakeAPI struct { + method string + path string + body any + + resp *hey.Response + err error +} + +func (f *fakeAPI) record(method, path string, body any) (*hey.Response, error) { + f.method, f.path, f.body = method, path, body + if f.resp == nil && f.err == nil { + return &hey.Response{Data: json.RawMessage(`{"ok":true}`), StatusCode: 200}, nil + } + return f.resp, f.err +} + +func (f *fakeAPI) Get(_ context.Context, path string) (*hey.Response, error) { + return f.record("GET", path, nil) +} +func (f *fakeAPI) Post(_ context.Context, path string, body any) (*hey.Response, error) { + return f.record("POST", path, body) +} +func (f *fakeAPI) Put(_ context.Context, path string, body any) (*hey.Response, error) { + return f.record("PUT", path, body) +} +func (f *fakeAPI) Patch(_ context.Context, path string, body any) (*hey.Response, error) { + return f.record("PATCH", path, body) +} +func (f *fakeAPI) Delete(_ context.Context, path string) (*hey.Response, error) { + return f.record("DELETE", path, nil) +} + +func op() *catalog.Operation { + return &catalog.Operation{ + ID: "GetBox", + Action: "get_box", + Method: "GET", + Path: "/boxes/{boxId}", + Params: []catalog.Param{ + {Name: "boxId", In: "path", Required: true}, + {Name: "page", In: "query"}, + }, + } +} + +func TestBuildRequestSubstitutesAndEscapesPathParams(t *testing.T) { + path, body, err := buildRequest(op(), map[string]any{"boxId": "im box/1"}) + if err != nil { + t.Fatal(err) + } + if path != "/boxes/im%20box%2F1" { + t.Errorf("path = %q", path) + } + if body != nil { + t.Errorf("body = %v, want nil", body) + } +} + +func TestBuildRequestFormatsScalars(t *testing.T) { + path, _, err := buildRequest(op(), map[string]any{"boxId": float64(42), "page": float64(3)}) + if err != nil { + t.Fatal(err) + } + if path != "/boxes/42?page=3" { + t.Errorf("path = %q", path) + } +} + +func TestBuildRequestMissingRequiredQueryParam(t *testing.T) { + required := op() + required.Params = append(required.Params, catalog.Param{Name: "since", In: "query", Required: true}) + _, _, err := buildRequest(required, map[string]any{"boxId": "1"}) + if err == nil || !strings.Contains(err.Error(), `missing required query parameter "since"`) { + t.Fatalf("err = %v", err) + } +} + +func TestBuildRequestMissingPathParam(t *testing.T) { + _, _, err := buildRequest(op(), map[string]any{}) + if err == nil || !strings.Contains(err.Error(), `missing required path parameter "boxId"`) { + t.Fatalf("err = %v", err) + } +} + +func TestBuildRequestRejectsNonScalarPathParam(t *testing.T) { + _, _, err := buildRequest(op(), map[string]any{"boxId": []any{1}}) + if err == nil || !strings.Contains(err.Error(), `path parameter "boxId"`) { + t.Fatalf("err = %v", err) + } +} + +func TestBuildRequestRejectsStrayParamsWithoutBody(t *testing.T) { + _, _, err := buildRequest(op(), map[string]any{"boxId": "1", "bogus": "x"}) + if err == nil || !strings.Contains(err.Error(), `unknown parameter "bogus"`) { + t.Fatalf("err = %v", err) + } +} + +func bodyOp() *catalog.Operation { + return &catalog.Operation{ + ID: "CreateBoxDesignation", + Action: "create_box_designation", + Method: "POST", + Path: "/boxes/{boxId}/designations.json", + Params: []catalog.Param{{Name: "boxId", In: "path", Required: true}}, + Body: map[string]any{ + "type": "object", + "properties": map[string]any{ + "posting_ids": map[string]any{"type": "array"}, + }, + }, + BodyRequired: true, + } +} + +func TestBuildRequestGathersBodyFromRemainingParams(t *testing.T) { + path, body, err := buildRequest(bodyOp(), map[string]any{ + "boxId": "7", + "posting_ids": []any{float64(1), float64(2)}, + }) + if err != nil { + t.Fatal(err) + } + if path != "/boxes/7/designations.json" { + t.Errorf("path = %q", path) + } + want := map[string]any{"posting_ids": []any{float64(1), float64(2)}} + if !reflect.DeepEqual(body, want) { + t.Errorf("body = %#v, want %#v", body, want) + } +} + +func TestBuildRequestRejectsBodyPropertyOutsideSchema(t *testing.T) { + _, _, err := buildRequest(bodyOp(), map[string]any{"boxId": "7", "bogus": "x"}) + if err == nil || !strings.Contains(err.Error(), `unknown parameter "bogus"`) { + t.Fatalf("err = %v", err) + } +} + +func TestBuildRequestSendsEmptyRequiredBody(t *testing.T) { + _, body, err := buildRequest(bodyOp(), map[string]any{"boxId": "7"}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(body, map[string]any{}) { + t.Errorf("body = %#v, want empty map", body) + } +} + +func TestDispatchCallRoutesByMethod(t *testing.T) { + for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} { + api := &fakeAPI{} + if _, err := (dispatcher{api: api}).call(context.Background(), method, "/x.json", nil); err != nil { + t.Fatalf("%s: %v", method, err) + } + if api.method != method { + t.Errorf("recorded method = %q, want %q", api.method, method) + } + } + + if _, err := (dispatcher{api: &fakeAPI{}}).call(context.Background(), "TRACE", "/x.json", nil); err == nil { + t.Error("unsupported method did not error") + } +} + +func TestDispatchAPIErrorIsInBand(t *testing.T) { + cat := loadForTest(t) + boxes := cat.Domains[0] + + api := &fakeAPI{err: errors.New("api: box not found")} + result, err := (dispatcher{api: api}).handle(context.Background(), boxes, mustFind(t, boxes, "list_boxes"), map[string]any{}) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("API failure did not produce an isError result") + } +} + +func TestDispatchSurfacesNextPageCursor(t *testing.T) { + cat := loadForTest(t) + boxes := cat.Domains[0] + + api := &fakeAPI{resp: &hey.Response{ + Data: json.RawMessage(`[{"id":1}]`), + StatusCode: 200, + Headers: http.Header{"Link": []string{ + `; rel="next"`, + }}, + }} + result, err := (dispatcher{api: api}).handle(context.Background(), boxes, mustFind(t, boxes, "list_boxes"), map[string]any{}) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("unexpected isError: %v", result.Content) + } + var wrapped struct { + NextPage string `json:"next_page"` + Results json.RawMessage `json:"results"` + } + if err := json.Unmarshal([]byte(textContent(t, result)), &wrapped); err != nil { + t.Fatal(err) + } + if wrapped.NextPage != "abc123" { + t.Errorf("next_page = %q, want abc123", wrapped.NextPage) + } + if string(wrapped.Results) != `[{"id":1}]` { + t.Errorf("results = %s", wrapped.Results) + } +} + +func TestDispatchChangesFeedFinalCursor(t *testing.T) { + cat := loadForTest(t) + boxes := cat.Domains[0] + + api := &fakeAPI{resp: &hey.Response{ + Data: json.RawMessage(`{"added":[],"updated":[],"deleted":[]}`), + StatusCode: 200, + Headers: http.Header{"Link": []string{ + `; rel="next"`, + }}, + }} + result, err := (dispatcher{api: api}).handle(context.Background(), boxes, mustFind(t, boxes, "get_box_posting_changes"), + map[string]any{"boxId": "1", "since": "2026-08-28T09:00:00.000Z"}) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("unexpected isError: %v", result.Content) + } + var wrapped struct { + NextPage string `json:"next_page"` + NextSince string `json:"next_since"` + NextV string `json:"next_v"` + Results json.RawMessage `json:"results"` + } + if err := json.Unmarshal([]byte(textContent(t, result)), &wrapped); err != nil { + t.Fatal(err) + } + if wrapped.NextPage != "" { + t.Errorf("next_page = %q, want empty on the final changes page", wrapped.NextPage) + } + if wrapped.NextSince != "2026-08-28T10:00:00.000Z" { + t.Errorf("next_since = %q, want 2026-08-28T10:00:00.000Z", wrapped.NextSince) + } + if wrapped.NextV != "5" { + t.Errorf("next_v = %q, want 5", wrapped.NextV) + } + if string(wrapped.Results) != `{"added":[],"updated":[],"deleted":[]}` { + t.Errorf("results = %s", wrapped.Results) + } +} + +func TestDispatchLastPageStaysUnwrapped(t *testing.T) { + cat := loadForTest(t) + boxes := cat.Domains[0] + + api := &fakeAPI{resp: &hey.Response{Data: json.RawMessage(`[{"id":1}]`), StatusCode: 200}} + result, err := (dispatcher{api: api}).handle(context.Background(), boxes, mustFind(t, boxes, "list_boxes"), map[string]any{}) + if err != nil { + t.Fatal(err) + } + if text := textContent(t, result); text != `[{"id":1}]` { + t.Errorf("result = %q, want the raw listing", text) + } +} + +func TestDispatchBodyCursorFromNextHistoryURL(t *testing.T) { + cat := loadForTest(t) + boxes := cat.Domains[0] + + for name, data := range map[string]string{ + "flat": `{"id":1,"next_history_url":"https://app.hey.com/boxes/1?page=zzz9"}`, + "nested": `{"box":{"id":1,"next_history_url":"https://app.hey.com/boxes/1?page=zzz9"}}`, + } { + api := &fakeAPI{resp: &hey.Response{Data: json.RawMessage(data), StatusCode: 200}} + result, err := (dispatcher{api: api}).handle(context.Background(), boxes, mustFind(t, boxes, "get_box"), map[string]any{"boxId": "1"}) + if err != nil { + t.Fatal(err) + } + var wrapped struct { + NextPage string `json:"next_page"` + } + if err := json.Unmarshal([]byte(textContent(t, result)), &wrapped); err != nil { + t.Fatal(err) + } + if wrapped.NextPage != "zzz9" { + t.Errorf("%s: next_page = %q, want zzz9", name, wrapped.NextPage) + } + } +} + +func TestDispatchEmptyResponseWithLocation(t *testing.T) { + cat := loadForTest(t) + threads := domainByKey(t, cat, "threads") + + api := &fakeAPI{resp: &hey.Response{ + StatusCode: 204, + Headers: http.Header{"Location": []string{"https://app.hey.com/messages/987"}}, + }} + result, err := (dispatcher{api: api}).handle(context.Background(), threads, mustFind(t, threads, "create_message"), map[string]any{}) + if err != nil { + t.Fatal(err) + } + text := textContent(t, result) + if !strings.Contains(text, "204") || !strings.Contains(text, "messages/987") { + t.Errorf("result = %q, want status and location", text) + } +} + +func TestDispatchEmptyResponseReportsStatus(t *testing.T) { + cat := loadForTest(t) + boxes := cat.Domains[0] + + api := &fakeAPI{resp: &hey.Response{StatusCode: 204}} + result, err := (dispatcher{api: api}).handle(context.Background(), boxes, mustFind(t, boxes, "get_box"), map[string]any{"boxId": "1"}) + if err != nil { + t.Fatal(err) + } + if result.IsError { + t.Fatalf("unexpected isError: %v", result.Content) + } + text := textContent(t, result) + if !strings.Contains(text, "204") { + t.Errorf("result = %q, want status report", text) + } +} diff --git a/internal/mcpserver/domains.go b/internal/mcpserver/domains.go new file mode 100644 index 00000000..a0465974 --- /dev/null +++ b/internal/mcpserver/domains.go @@ -0,0 +1,51 @@ +package mcpserver + +import "github.com/basecamp/mcp/catalog" + +// DomainSpecs curates which slice of the hey-sdk surface each domain gateway +// tool exposes, in tool display order. Tags are hey-sdk's OpenAPI tags (each +// operation carries exactly one); a spec may merge several tags into one +// tool. This mapping is the only hand-maintained part of the catalog — +// everything else derives from the SDK model via the toolkit. +// +// The first release serves five domains covering everyday mail and task +// work: boxes, search, threads, contacts, todos. Tags left unmapped are +// reported in Catalog.Unmapped and pinned by tests, so growing the surface +// is a one-line change here plus a snapshot refresh. +var DomainSpecs = []catalog.DomainSpec{ + { + Key: "boxes", + Tags: []string{"Boxes"}, + Blurb: "HEY mail boxes: the Imbox, Feed, Paper Trail, Reply Later, Set Aside and Bubble Up stacks, box groups and designations, and incremental posting changes.", + }, + { + Key: "search", + Tags: []string{"Search"}, + Blurb: "Search HEY mail: advanced search with the same refinements the search page offers.", + }, + { + Key: "threads", + Tags: []string{"Topics", "Entries", "Messages"}, + Blurb: "HEY email threads: topics and their entries, full message content, replies and forwards, drafts, and triage (trash, spam, restore, move).", + }, + { + Key: "contacts", + Tags: []string{"Contacts"}, + Blurb: "HEY contacts and the Screener: contact records and notes, bundling, and clearance (screening) decisions.", + }, + { + Key: "todos", + Tags: []string{"Calendar Todos"}, + Blurb: "HEY Calendar todos: create, update, complete, uncomplete, and delete. Read existing todos through the calendar domain's get_calendar_recordings.", + }, + { + Key: "calendar", + Tags: []string{"Calendars"}, + Blurb: "HEY Calendars: list calendars, read their recordings (todos and events — the todo read path), and toggle calendar visibility.", + }, + { + Key: "identity", + Tags: []string{"Identity"}, + Blurb: "Your HEY identity: accounts, senders, and preferences — the acting_sender_id and acting_user_id lookups that replies and contact writes ask for.", + }, +} diff --git a/internal/mcpserver/model/PROVENANCE.json b/internal/mcpserver/model/PROVENANCE.json new file mode 100644 index 00000000..b6f7ac26 --- /dev/null +++ b/internal/mcpserver/model/PROVENANCE.json @@ -0,0 +1,7 @@ +{ + "source": "github.com/basecamp/hey-sdk", + "commit": "cbc342f6419eb7d9cf703d25efc43c866bfc4ab6", + "ref": "go/v0.28.0", + "files": ["behavior-model.json", "openapi.json"], + "synced_by": "scripts/sync-mcp-model.sh" +} diff --git a/internal/mcpserver/model/behavior-model.json b/internal/mcpserver/model/behavior-model.json new file mode 100644 index 00000000..3120de69 --- /dev/null +++ b/internal/mcpserver/model/behavior-model.json @@ -0,0 +1,1700 @@ +{ + "$schema": "https://hey.com/schemas/behavior-model.json", + "generated": true, + "operations": { + "AddPostingsToBoxGroup": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "AdvancedSearch": { + "idempotent": false, + "pagination": { + "style": "link" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "BubbleUpPostingsNow": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "BulkUpdateClearances": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "BundleContact": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CancelPostingsBubbleUp": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CompleteCalendarTodo": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "CompleteHabit": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateBoxDesignation": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateBoxGroup": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateBulkReply": { + "idempotent": false, + "readonly": false, + "retry": { + "max": 0 + } + }, + "CreateCalendarTodo": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateContact": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateDirectUpload": { + "idempotent": false, + "readonly": false, + "retry": { + "max": 0 + } + }, + "CreateFolderForPostings": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateHabit": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateMessage": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateReply": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateSticky": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateTimeTrack": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "CreateWorkflowStaging": { + "idempotent": false, + "readonly": false, + "retry": { + "max": 0 + } + }, + "DeleteBoxDesignation": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteBoxGroup": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteCalendarTodo": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteContactNote": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteDraft": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteHabit": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteSticky": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "DeleteTimeTrack": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "EmptySpam": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "EmptyTrash": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "FilePostings": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetAdvancedSearchFilters": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetAsidebox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetBox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetBoxPostingChanges": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetBubblebox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetBundleUnseenPostings": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetCalendarDay": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetCalendarRecordings": { + "idempotent": false, + "pagination": { + "style": "window" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetCalendarWeek": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetCalendarYear": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetClearances": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetCollection": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetContact": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetContactNote": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetEverythingTopics": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetFeedbox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetFolder": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetIdentity": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetImbox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetImboxSeen": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetJournalEntry": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetLaterbox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetMessage": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetMessageEdit": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetMyClearances": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetNavigation": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetOngoingTimeTrack": { + "empty_on": [ + 404 + ], + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetSentTopics": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetSpamTopics": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetTopic": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetTopicEntries": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetTopicPublication": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetTrailbox": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetTrashTopics": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "GetWorkflow": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "HideContact": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListBoxGroups": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListBoxes": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListCalendarDays": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListCalendarWeeks": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListCalendars": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListClips": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListCollections": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListContacts": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListDrafts": { + "idempotent": false, + "pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListJournalEntries": { + "idempotent": false, + "pagination": { + "style": "link" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListSnippets": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListStickies": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListTimeTrackCategories": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "ListTimeTracks": { + "idempotent": false, + "pagination": { + "style": "link" + }, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "MarkBoxSeen": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MarkEntrySpam": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MarkPostingsSeen": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MarkPostingsSpam": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MarkPostingsUnseen": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MarkTopicHam": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MovePostings": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MoveSticky": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MoveTopic": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "MoveWorkflowStaging": { + "idempotent": false, + "readonly": false, + "retry": { + "max": 0 + } + }, + "MutePostings": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "NewBulkReply": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "NewEntryForward": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "NewEntryReply": { + "idempotent": false, + "readonly": true, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "PuntClearances": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "RemovePostingsFromBoxGroup": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "RestoreTopic": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "ResumeHabit": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "RevealContact": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "SchedulePostingsBubbleUp": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "StartTimeTrack": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "StopHabit": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "ToggleCalendar": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "TrashPostings": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "TrashTopic": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UnbundleContact": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UncompleteCalendarTodo": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "UncompleteHabit": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "UnfilePostings": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UnmutePostings": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateCalendarTodo": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateClearance": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateCollection": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateContact": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateContactClearance": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateContactNote": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateFirstWeekDay": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateHabit": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateJournalEntry": { + "idempotent": false, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateMessage": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateMyClearance": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateSticky": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 2, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateTimeFormat": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + }, + "UpdateTimeTrack": { + "idempotent": true, + "readonly": false, + "retry": { + "backoff": "exponential", + "base_delay_ms": 1000, + "max": 3, + "retry_on": [ + 429, + 503 + ] + } + } + }, + "version": "1.0.0" +} diff --git a/internal/mcpserver/model/openapi.json b/internal/mcpserver/model/openapi.json new file mode 100644 index 00000000..6cc8b534 --- /dev/null +++ b/internal/mcpserver/model/openapi.json @@ -0,0 +1,14035 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "HEY", + "version": "2026-08-21", + "description": "HEY API", + "contact": { + "name": "Basecamp", + "url": "https://github.com/basecamp/hey-sdk" + }, + "license": { + "name": "MIT", + "url": "https://github.com/basecamp/hey-sdk/blob/main/LICENSE" + } + }, + "paths": { + "/advanced_search.json": { + "get": { + "description": "Get the options the advanced search refine form offers.\n\nAdvanced search: message matches grouped by topic as the search page shows them —\nthe topic, its posting id, and the matching entries as summaries (no bodies; read a\nmessage with GetMessage). Refinements are the same query parameters the page uses.\nThe next page, if any, is a Link header.", + "operationId": "AdvancedSearch", + "parameters": [ + { + "name": "q", + "in": "query", + "description": "The words to search for", + "schema": { + "type": "string", + "description": "The words to search for", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[from]", + "in": "query", + "description": "Refinements, e.g. refine[from], refine[to], refine[subject], refine[exact_phrase],\nrefine[required], refine[any], refine[none], refine[date], refine[in], refine[label],\nrefine[attachment] — passed through as the page sends them.", + "schema": { + "type": "string", + "description": "Refinements, e.g. refine[from], refine[to], refine[subject], refine[exact_phrase],\nrefine[required], refine[any], refine[none], refine[date], refine[in], refine[label],\nrefine[attachment] — passed through as the page sends them.", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[to]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[subject]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[exact_phrase]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[required]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[any]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[none]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[date]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[in]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[label]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "refine[attachment]", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "AdvancedSearch 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdvancedSearchResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Search" + ], + "x-hey-pagination": { + "style": "link" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/advanced_search_filters.json": { + "get": { + "description": "The advanced search refine form's options: boxes, date ranges, labels and attachment kinds.", + "operationId": "GetAdvancedSearchFilters", + "responses": { + "200": { + "description": "GetAdvancedSearchFilters 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAdvancedSearchFiltersResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Search" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes.json": { + "get": { + "description": "List all boxes", + "operationId": "ListBoxes", + "responses": { + "200": { + "description": "ListBoxes 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListBoxesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}": { + "get": { + "description": "Get a specific box", + "operationId": "GetBox", + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetBox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBoxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}/designations.json": { + "post": { + "description": "Designate a contact to a box, so everything they send lands there", + "operationId": "CreateBoxDesignation", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBoxDesignationRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateBoxDesignation 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}/designations/{designationId}": { + "delete": { + "description": "Remove a designation from a box. The id is the designation's, not the contact's.", + "operationId": "DeleteBoxDesignation", + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "designationId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteBoxDesignation 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}/groups.json": { + "get": { + "description": "List the Set Aside groups in a box", + "operationId": "ListBoxGroups", + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ListBoxGroups 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListBoxGroupsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Create a Set Aside group out of a selection of postings.\n\nThis endpoint does not split a comma-joined posting_ids string — send an array.", + "operationId": "CreateBoxGroup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBoxGroupRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateBoxGroup 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBoxGroupResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}/groups/{groupId}": { + "delete": { + "description": "Break up a Set Aside group, moving its postings back to Previously Seen", + "operationId": "DeleteBoxGroup", + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "groupId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteBoxGroup 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}/observation.json": { + "post": { + "description": "Mark everything in a box as seen. The work is queued, so the effect is eventually consistent.", + "operationId": "MarkBoxSeen", + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "MarkBoxSeen 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/boxes/{boxId}/postings/changes.json": { + "get": { + "description": "Read what changed among a box's postings since a point in time.\n\nThis is the incremental sync feed the mail clients follow rather than re-reading a\nbox. `since` is an ISO 8601 timestamp with milliseconds and is exclusive, and `v` is\nthe client's contract version — the server answers 409 when the caller is too far\nbehind for an increment to carry the difference, which means read the box in full\ninstead. A box's own `posting_changes_url` carries the `since` and `v` to start from,\nand the `Link` header names the next page while one remains and the next `since`\ncursor on the last page.", + "operationId": "GetBoxPostingChanges", + "parameters": [ + { + "name": "boxId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "since", + "in": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "v", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetBoxPostingChanges 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBoxPostingChangesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/bubble_up.json": { + "get": { + "description": "Get the Bubble Up box", + "operationId": "GetBubblebox", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetBubblebox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBubbleboxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/bulk_replies.json": { + "post": { + "description": "Send one reply to every entry. Answers what was sent, not the replies themselves:\ndelivery is queued, and delayed while undo is still possible.", + "operationId": "CreateBulkReply", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkReplyRequestContent" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "CreateBulkReply 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBulkReplyResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Bulk Reply" + ] + } + }, + "/bulk_replies/new.json": { + "get": { + "description": "Work out which entries a bulk reply would answer. HEY replies to the last replyable\nentry of each thread, skipping threads with no reply address, so the postings you hold\nare not the entries you send to — this resolves them.", + "operationId": "NewBulkReply", + "parameters": [ + { + "name": "posting_ids", + "in": "query", + "description": "The postings to reply to, comma separated.", + "schema": { + "type": "string", + "description": "The postings to reply to, comma separated." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "NewBulkReply 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewBulkReplyResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Bulk Reply" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/days.json": { + "get": { + "description": "List the days from a date onwards. The server picks how many, so this is a window\nrather than a page: read the next one by asking from the last day's date.", + "operationId": "ListCalendarDays", + "parameters": [ + { + "name": "starts_at", + "in": "query", + "description": "Date (YYYY-MM-DD) to start from. Defaults to today.", + "schema": { + "type": "string", + "description": "Date (YYYY-MM-DD) to start from. Defaults to today.", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListCalendarDays 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCalendarDaysResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Periods" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/days/{day}": { + "get": { + "description": "Get one day", + "operationId": "GetCalendarDay", + "parameters": [ + { + "name": "day", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetCalendarDay 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCalendarDayResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Periods" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/days/{day}/habits/{habitId}/completions": { + "delete": { + "description": "Uncomplete a habit for a day", + "operationId": "UncompleteHabit", + "parameters": [ + { + "name": "day", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "habitId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UncompleteHabit 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UncompleteHabitResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Complete a habit for a day", + "operationId": "CompleteHabit", + "parameters": [ + { + "name": "day", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "habitId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CompleteHabit 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompleteHabitResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-idempotent": { + "natural": true + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/days/{day}/journal_entry": { + "get": { + "description": "Get journal entry for a day", + "operationId": "GetJournalEntry", + "parameters": [ + { + "name": "day", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetJournalEntry 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetJournalEntryResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Journal" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Update the journal entry for a day: writes (or creates) it and answers the entry as a\nrecording, or 204 when empty content removes it.", + "operationId": "UpdateJournalEntry", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateJournalEntryRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "day", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateJournalEntry 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateJournalEntryResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Journal" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/habits.json": { + "post": { + "description": "Start a new habit. Answers the created habit as a recording.", + "operationId": "CreateHabit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HabitRequestContent" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "CreateHabit 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateHabitResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/habits/{habitId}": { + "delete": { + "description": "Delete a habit. habitId is the recording's id.", + "operationId": "DeleteHabit", + "parameters": [ + { + "name": "habitId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteHabit 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Edit a habit. habitId is the recording's id.", + "operationId": "UpdateHabit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HabitRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "habitId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateHabit 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateHabitResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/habits/{habitId}/stop.json": { + "delete": { + "description": "Resume a paused habit", + "operationId": "ResumeHabit", + "parameters": [ + { + "name": "habitId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ResumeHabit 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Pause a habit, so it stops appearing on the calendar", + "operationId": "StopHabit", + "parameters": [ + { + "name": "habitId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "StopHabit 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Habits" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/identity/first_week_day": { + "put": { + "description": "Set which day the identity's calendar weeks start on. Answers the stored\npreference. The write reaches every HEY client — web, mobile and this SDK\nread the same identity preference.", + "operationId": "UpdateFirstWeekDay", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFirstWeekDayRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "UpdateFirstWeekDay 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFirstWeekDayResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Identity" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/journal_entries": { + "get": { + "description": "List journal entries newest first. The next page, if any, is a Link header.\nPass q to search journal entry content.", + "operationId": "ListJournalEntries", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListJournalEntries 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListJournalEntriesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Journal" + ], + "x-hey-pagination": { + "style": "link" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/ongoing_time_track.json": { + "get": { + "description": "Get the ongoing time track (404 = no active track; see ADR-004)", + "operationId": "GetOngoingTimeTrack", + "responses": { + "200": { + "description": "GetOngoingTimeTrack 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOngoingTimeTrackResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-empty-on": { + "statusCodes": [ + 404 + ] + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Start a new time track. Takes no body: haystack's\nCalendar::OngoingTimeTracksController#create ignores request parameters and\nstarts a track with defaults; use UpdateTimeTrack to set notes and category_title,\nwhich also stops the track.", + "operationId": "StartTimeTrack", + "responses": { + "200": { + "description": "StartTimeTrack 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartTimeTrackResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/time_tracks.json": { + "get": { + "description": "List tracked time — completed tracks only, newest-ended first.\n\nA running track is not here; read that with GetOngoingTimeTrack. The next page, if\nany, is a Link header, and the last page carries none, so a nil Link is the end of\nthe list rather than an error.\n\ncategory_id narrows the list to one category and 404s if the calendar has no\ncategory by that id.\n\nThe calendar's categories come back alongside the tracks, so showing or applying the\nfilter does not need ListTimeTrackCategories as well.", + "operationId": "ListTimeTracks", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "category_id", + "in": "query", + "schema": { + "type": "integer", + "format": "int64", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListTimeTracks 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListTimeTracksResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-pagination": { + "style": "link" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Record a finished stretch of time.\n\nJSON callers send the fields flat; Rails wraps them into calendar_time_track itself.", + "operationId": "CreateTimeTrack", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeTrackRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateTimeTrack 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTimeTrackResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/time_tracks/categories.json": { + "get": { + "description": "List the calendar's time track categories, alphabetically", + "operationId": "ListTimeTrackCategories", + "responses": { + "200": { + "description": "ListTimeTrackCategories 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListTimeTrackCategoriesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/time_tracks/{timeTrackId}": { + "delete": { + "description": "Delete a time track. The id is the recording's.", + "operationId": "DeleteTimeTrack", + "parameters": [ + { + "name": "timeTrackId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteTimeTrack 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "put": { + "description": "Update a time track (stop by setting ends_at to current time).\n\nEvery update completes the track, whether or not ends_at is sent, so this cannot\nbe used to adjust a running track: it stops it.\n\nOnly the fields sent are written, so a partial update leaves the rest of the track\nalone. A starts_at or ends_at the server cannot parse is a 400, not a 422.", + "operationId": "UpdateTimeTrack", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTimeTrackRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "timeTrackId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateTimeTrack 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTimeTrackResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Time Tracks" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/todos.json": { + "post": { + "description": "Create a calendar todo", + "operationId": "CreateCalendarTodo", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarTodoRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateCalendarTodo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarTodoResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Todos" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/todos/{todoId}": { + "delete": { + "description": "Delete a calendar todo", + "operationId": "DeleteCalendarTodo", + "parameters": [ + { + "name": "todoId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteCalendarTodo 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Todos" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Edit a calendar todo. todoId is the recording's id, and every field of the payload\nis optional: haystack's `wrap_parameters` accepts title, focused and starts_at, and\nchanges only what is sent.", + "operationId": "UpdateCalendarTodo", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCalendarTodoRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "todoId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateCalendarTodo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCalendarTodoResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Todos" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/todos/{todoId}/completions": { + "delete": { + "description": "Uncomplete a calendar todo", + "operationId": "UncompleteCalendarTodo", + "parameters": [ + { + "name": "todoId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UncompleteCalendarTodo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UncompleteCalendarTodoResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Todos" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Complete a calendar todo", + "operationId": "CompleteCalendarTodo", + "parameters": [ + { + "name": "todoId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CompleteCalendarTodo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompleteCalendarTodoResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Todos" + ], + "x-hey-idempotent": { + "natural": true + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/weeks.json": { + "get": { + "description": "List the weeks around a date — nine of them, centered on it.", + "operationId": "ListCalendarWeeks", + "parameters": [ + { + "name": "starts_at", + "in": "query", + "description": "Date (YYYY-MM-DD) of the first week. Takes precedence over centered_at.", + "schema": { + "type": "string", + "description": "Date (YYYY-MM-DD) of the first week. Takes precedence over centered_at.", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "centered_at", + "in": "query", + "description": "Date (YYYY-MM-DD) to center the nine weeks on. Defaults to today.", + "schema": { + "type": "string", + "description": "Date (YYYY-MM-DD) to center the nine weeks on. Defaults to today.", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListCalendarWeeks 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCalendarWeeksResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Periods" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/weeks/{week}": { + "get": { + "description": "Get one week", + "operationId": "GetCalendarWeek", + "parameters": [ + { + "name": "week", + "in": "path", + "description": "Any date in the week (YYYY-MM-DD)", + "schema": { + "type": "string", + "description": "Any date in the week (YYYY-MM-DD)" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetCalendarWeek 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCalendarWeekResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Periods" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendar/years/{year}": { + "get": { + "description": "Get one year as the grid it is drawn as", + "operationId": "GetCalendarYear", + "parameters": [ + { + "name": "year", + "in": "path", + "description": "Any date in the year (YYYY-MM-DD)", + "schema": { + "type": "string", + "description": "Any date in the year (YYYY-MM-DD)" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetCalendarYear 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCalendarYearResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendar Periods" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendars.json": { + "get": { + "description": "List calendars", + "operationId": "ListCalendars", + "responses": { + "200": { + "description": "ListCalendars 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCalendarsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendars" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendars/{calendarId}/recordings": { + "get": { + "description": "Get recordings for a calendar", + "operationId": "GetCalendarRecordings", + "parameters": [ + { + "name": "calendarId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "starts_on", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "ends_on", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetCalendarRecordings 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCalendarRecordingsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendars" + ], + "x-hey-pagination": { + "style": "window" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/calendars/{calendarId}/toggle": { + "post": { + "description": "Switch a calendar in or out of the reader's selection, and answer the selection it\nleft behind. The selection is what every period read is scoped to, so a toggle is how\na client changes which calendars a day, week or year is drawn from.", + "operationId": "ToggleCalendar", + "parameters": [ + { + "name": "calendarId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ToggleCalendar 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToggleCalendarResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Calendars" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/clearances.json": { + "get": { + "description": "Get the Screener — the pending count, and the senders waiting when asked for them", + "operationId": "GetClearances", + "parameters": [ + { + "name": "include_clearances", + "in": "query", + "schema": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetClearances 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetClearancesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/clearances/bulk.json": { + "patch": { + "description": "Screen several senders out at once. ids is a comma-separated list.", + "operationId": "BulkUpdateClearances", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkUpdateClearancesRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "BulkUpdateClearances 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkUpdateClearancesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/clearances/punt.json": { + "post": { + "description": "Clear the Screener — every pending sender is punted and reexamined on their next email", + "operationId": "PuntClearances", + "responses": { + "200": { + "description": "PuntClearances 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/clearances/{clearanceId}": { + "patch": { + "description": "Screen a sender in or out of the Screener\n\ndesignation_box_id files everything they send into that box instead of the Imbox.\nspam marks what is already waiting as spam and trains the filter on it.", + "operationId": "UpdateClearance", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateClearanceRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "clearanceId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateClearance 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateClearanceResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/clips.json": { + "get": { + "description": "List clips, newest first", + "operationId": "ListClips", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListClips 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListClipsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Clips" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/collections.json": { + "get": { + "description": "List collections", + "operationId": "ListCollections", + "responses": { + "200": { + "description": "ListCollections 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCollectionsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Collections" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/collections/{collectionId}": { + "get": { + "description": "Get a collection and one page of its active, accessible threads", + "operationId": "GetCollection", + "parameters": [ + { + "name": "collectionId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetCollection 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCollectionResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Collections" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Rename a collection or change its summary", + "operationId": "UpdateCollection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCollectionRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "collectionId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateCollection 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Collections" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/contacts.json": { + "get": { + "description": "List contacts", + "operationId": "ListContacts", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListContacts 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListContactsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Add a contact. Answers the contact that was created.", + "operationId": "CreateContact", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateContactRequestContent" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "CreateContact 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateContactResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/contacts/{contactId}": { + "delete": { + "description": "Hide a contact. Nothing is deleted — RevealContact brings them back.", + "operationId": "HideContact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "HideContact 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "get": { + "description": "Get a contact, with a page of the threads they are on", + "operationId": "GetContact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetContact 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetContactResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Edit a contact. HEY rewrites the whole contact, so send every field: a name,\naddress or alias left out is cleared. Answers the contact, which is not always\nthe one addressed — promoting an alias makes the alias primary.", + "operationId": "UpdateContact", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContactRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateContact 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateContactResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/contacts/{contactId}/bundle.json": { + "delete": { + "description": "Stop bundling a contact's mail", + "operationId": "UnbundleContact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UnbundleContact 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Bundle a contact so their mail arrives grouped", + "operationId": "BundleContact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "BundleContact 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/contacts/{contactId}/clearance.json": { + "patch": { + "description": "Screen a contact in or out. Status is \"approved\" or \"denied\".", + "operationId": "UpdateContactClearance", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateContactClearanceRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateContactClearance 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/contacts/{contactId}/note.json": { + "delete": { + "description": "Clear the private note on a contact", + "operationId": "DeleteContactNote", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteContactNote 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "get": { + "description": "Read the private note kept on a contact", + "operationId": "GetContactNote", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetContactNote 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetContactNoteResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Write the private note on a contact, replacing whatever was there", + "operationId": "UpdateContactNote", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContactNoteRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateContactNote 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateContactNoteResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/contacts/{contactId}/reveal.json": { + "post": { + "description": "Put a hidden contact back in the contact list", + "operationId": "RevealContact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "RevealContact 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevealContactResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/entries/drafts.json": { + "get": { + "description": "List draft messages", + "operationId": "ListDrafts", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListDrafts 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListDraftsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Entries" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/entries/drafts/{entryId}": { + "delete": { + "description": "Trash a draft (Entries::DraftsController#destroy). The id is the draft's entry id,\nas ListDrafts reports it.", + "operationId": "DeleteDraft", + "parameters": [ + { + "name": "entryId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteDraft 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Entries" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/entries/{entryId}/forwards/new.json": { + "get": { + "description": "Get a prefilled forward of an entry: subject, quoted body and blank recipients.\nSend it with CreateMessage once the recipients are filled in.", + "operationId": "NewEntryForward", + "parameters": [ + { + "name": "entryId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "NewEntryForward 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewEntryForwardResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Entries" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/entries/{entryId}/replies.json": { + "post": { + "description": "Reply to an entry", + "operationId": "CreateReply", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReplyRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "entryId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateReply 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Entries" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/entries/{entryId}/replies/new.json": { + "get": { + "description": "Get a prefilled reply to an entry: the quoted body and, in addressed, the\nparticipating contacts a reply goes to as HEY computes them — the sender moved onto\nthe To line and the acting user's own addresses, aliases and catch-alls excluded.", + "operationId": "NewEntryReply", + "parameters": [ + { + "name": "entryId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "NewEntryReply 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewEntryReplyResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Entries" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/entries/{entryId}/status/spam.json": { + "put": { + "description": "Mark an entry as spam. Denies the sender when every thread from them is already spam.", + "operationId": "MarkEntrySpam", + "parameters": [ + { + "name": "entryId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "MarkEntrySpam 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Entries" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/feedbox.json": { + "get": { + "description": "Get the Feed", + "operationId": "GetFeedbox", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetFeedbox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFeedboxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/folders/{folderId}": { + "get": { + "description": "Get a folder (label) and the postings filed in it", + "operationId": "GetFolder", + "parameters": [ + { + "name": "folderId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetFolder 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFolderResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Folders" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/identity.json": { + "get": { + "description": "Get the current identity (authenticated user profile)", + "operationId": "GetIdentity", + "responses": { + "200": { + "description": "GetIdentity 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetIdentityResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Identity" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/identity/time_format": { + "put": { + "description": "Set whether HEY renders times on a 12-hour or a 24-hour clock. Answers the\nstored preference. The parameter is the web toggle's, said honestly: true\nfor the 24-hour clock, false for the 12-hour one.", + "operationId": "UpdateTimeFormat", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTimeFormatRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "UpdateTimeFormat 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTimeFormatResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Identity" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/imbox.json": { + "get": { + "description": "Get the Imbox", + "operationId": "GetImbox", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetImbox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetImboxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/imbox/seen.json": { + "get": { + "description": "Get the Imbox's Previously Seen postings", + "operationId": "GetImboxSeen", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetImboxSeen 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetImboxSeenResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/messages.json": { + "post": { + "description": "Create a new message (start a new topic).\nThe acting sender ID must be included; the Go SDK resolves this automatically.\nEvery message is created drafted on HEY's side; without entry.status the server\ndelivers it, while entry.status \"drafted\" leaves it as a draft and answers\n204 with a Location header naming /messages/{entry_id}.", + "operationId": "CreateMessage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMessageRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateMessage 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Messages" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/messages/{messageId}": { + "get": { + "description": "Get a message", + "operationId": "GetMessage", + "parameters": [ + { + "name": "messageId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetMessage 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMessageResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Messages" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "put": { + "description": "Revise a message entry (MessagesController#update). With entry.status \"drafted\" the\nentry is saved as a draft (204 + Location, like CreateMessage); without it a draft is\ndelivered through the undo-delay window. A trashed draft is silently restored first.\nThe revision is not a patch: subject, content and any scheduled delivery are rewritten\nfrom this request (an omitted scheduled delivery clears one), while recipients are\nreplaced only when entry.addressed is present.\n\nNot naturally idempotent despite the PUT: without the drafted status this request\n*delivers*, so a transparent retry after an ambiguous first attempt could send the\nmessage again. The client must not retry it.", + "operationId": "UpdateMessage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMessageRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "messageId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateMessage 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Messages" + ], + "x-hey-idempotent": { + "natural": false + }, + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/messages/{messageId}/edit.json": { + "get": { + "description": "A draft's editable state: content, recipients and scheduled delivery as the\ncomposer would load them (GET /messages/{id}/edit).", + "operationId": "GetMessageEdit", + "parameters": [ + { + "name": "messageId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetMessageEdit 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMessageEditResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Messages" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/my/clearances.json": { + "get": { + "description": "The senders already screened in or out", + "operationId": "GetMyClearances", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetMyClearances 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMyClearancesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/my/clearances/{clearanceId}": { + "patch": { + "description": "Rescreen a sender who was already screened in or out", + "operationId": "UpdateMyClearance", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMyClearanceRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "clearanceId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateMyClearance 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMyClearanceResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Contacts" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/my/navigation.json": { + "get": { + "description": "Get navigation items", + "operationId": "GetNavigation", + "responses": { + "200": { + "description": "GetNavigation 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNavigationResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Identity" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/paper_trail.json": { + "get": { + "description": "Get the Paper Trail", + "operationId": "GetTrailbox", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetTrailbox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTrailboxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/box_groups.json": { + "delete": { + "description": "Remove a selection of postings from their Set Aside group", + "operationId": "RemovePostingsFromBoxGroup", + "parameters": [ + { + "name": "posting_ids", + "in": "query", + "description": "Posting ids as a comma-joined string, for verbs that carry no body", + "schema": { + "type": "string", + "description": "Posting ids as a comma-joined string, for verbs that carry no body" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "RemovePostingsFromBoxGroup 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Add a selection of postings to a Set Aside group", + "operationId": "AddPostingsToBoxGroup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddPostingsToBoxGroupRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "AddPostingsToBoxGroup 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/bubble_up.json": { + "delete": { + "description": "Cancel a scheduled bubble up for a selection of postings", + "operationId": "CancelPostingsBubbleUp", + "parameters": [ + { + "name": "posting_ids", + "in": "query", + "description": "Posting ids as a comma-joined string, for verbs that carry no body", + "schema": { + "type": "string", + "description": "Posting ids as a comma-joined string, for verbs that carry no body" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CancelPostingsBubbleUp 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Schedule a selection of postings to bubble up.\n\nHEY's scheduler takes a `slot` — today, tomorrow, weekend, next_week, surprise_me\nor custom — and a custom slot also carries the `date` (YYYY-MM-DD) to bubble up on,\nat HEY's morning hour. The today slot lands at HEY's evening hour of the current\nday instead, and both hours are UTC over JSON. An unknown slot, or a custom slot\nwithout a date, is a server error rather than a validation response, so callers\ncheck both first. Responds 201 Created.", + "operationId": "SchedulePostingsBubbleUp", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulePostingsBubbleUpRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "SchedulePostingsBubbleUp 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/bulk_bubble_up_now.json": { + "post": { + "description": "Bubble a selection of postings up right now", + "operationId": "BubbleUpPostingsNow", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "BubbleUpPostingsNow 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/filings.json": { + "delete": { + "description": "Remove a selection of postings from a folder, or from every folder when folder_id is omitted", + "operationId": "UnfilePostings", + "parameters": [ + { + "name": "posting_ids", + "in": "query", + "description": "Posting ids as a comma-joined string, for verbs that carry no body", + "schema": { + "type": "string", + "description": "Posting ids as a comma-joined string, for verbs that carry no body" + }, + "required": true + }, + { + "name": "folder_id", + "in": "query", + "schema": { + "type": "integer", + "format": "int64", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "UnfilePostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "File a selection of postings into an existing folder (label)", + "operationId": "FilePostings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilePostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "FilePostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/folders.json": { + "post": { + "description": "Create a folder (label) and file a selection of postings into it", + "operationId": "CreateFolderForPostings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFolderForPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateFolderForPostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/moves.json": { + "post": { + "description": "Move postings to a box (bulk).\nMirrors HEY's Postings::MovesController: `posting_ids` plus the target `box_id`\n(an ID from ListBoxes; the box `kind` field identifies imbox, feedbox, asidebox,\nlaterbox, trailbox). Responds 204 No Content.", + "operationId": "MovePostings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MovePostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MovePostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/mutings.json": { + "delete": { + "description": "Unmute postings (bulk).\nMirrors HEY's Postings::MutingsController#destroy. `posting_ids` is sent as a\ncomma-separated query string because DELETE carries no body. Responds 201 Created.", + "operationId": "UnmutePostings", + "parameters": [ + { + "name": "posting_ids", + "in": "query", + "description": "Comma-separated posting IDs, e.g. \"123,456\"", + "schema": { + "type": "string", + "description": "Comma-separated posting IDs, e.g. \"123,456\"" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UnmutePostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Mute postings (bulk) — stop notifications for their threads.\nMirrors HEY's Postings::MutingsController#create. Responds 201 Created.", + "operationId": "MutePostings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MutePostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/seen.json": { + "post": { + "description": "Mark postings as seen", + "operationId": "MarkPostingsSeen", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MarkPostingsSeen 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/spam.json": { + "post": { + "description": "Mark a selection of postings as spam.\n\nOver ten postings the server hands the work to a background job, so the effect is\neventually consistent.", + "operationId": "MarkPostingsSpam", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MarkPostingsSpam 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/trash.json": { + "post": { + "description": "Move postings to the trash (bulk).\nMirrors HEY's Postings::TrashController. For JSON requests the server treats\nthe removal decision as made (shared topics: your access is removed).\nResponds 204 No Content.", + "operationId": "TrashPostings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrashPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "TrashPostings 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/unseen.json": { + "post": { + "description": "Mark postings as unseen", + "operationId": "MarkPostingsUnseen", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkPostingsRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MarkPostingsUnseen 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/postings/{postingId}/bundles/unseen.json": { + "get": { + "description": "List the unseen postings inside a bundle posting.\n\nA bundle posting groups one contact's unseen mail; this is its contents — the member\npostings, newest first, paged by cursor like a box. The posting must be a bundle.", + "operationId": "GetBundleUnseenPostings", + "parameters": [ + { + "name": "postingId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetBundleUnseenPostings 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBundleUnseenPostingsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Postings" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/rails/active_storage/direct_uploads.json": { + "post": { + "description": "Create an Active Storage direct upload for an outgoing attachment.\nThe returned URL is self-authenticating and accepts the raw file bytes via PUT.", + "operationId": "CreateDirectUpload", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDirectUploadRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateDirectUpload 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DirectUpload" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Attachments" + ] + } + }, + "/reply_later.json": { + "get": { + "description": "Get the Reply Later box", + "operationId": "GetLaterbox", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetLaterbox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetLaterboxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/set_aside.json": { + "get": { + "description": "Get the Set Aside box", + "operationId": "GetAsidebox", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetAsidebox 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAsideboxResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Boxes" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/snippets.json": { + "get": { + "description": "List snippets, alphabetically", + "operationId": "ListSnippets", + "responses": { + "200": { + "description": "ListSnippets 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSnippetsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Snippets" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/stickies.json": { + "get": { + "description": "List stickies, newest position first", + "operationId": "ListStickies", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Clamped server-side to 1..100", + "schema": { + "type": "integer", + "description": "Clamped server-side to 1..100", + "format": "int32", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "ListStickies 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListStickiesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Stickies" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "post": { + "description": "Write a new sticky", + "operationId": "CreateSticky", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickyRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateSticky 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateStickyResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Stickies" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/stickies/moves.json": { + "post": { + "description": "Reposition a sticky on the board", + "operationId": "MoveSticky", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MoveStickyRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MoveSticky 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Stickies" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/stickies/{stickyId}": { + "delete": { + "description": "Throw a sticky away", + "operationId": "DeleteSticky", + "parameters": [ + { + "name": "stickyId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteSticky 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Stickies" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + }, + "patch": { + "description": "Edit a sticky", + "operationId": "UpdateSticky", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickyRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "stickyId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateSticky 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStickyResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Stickies" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/everything.json": { + "get": { + "description": "Get all topics (everything view)", + "operationId": "GetEverythingTopics", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetEverythingTopics 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEverythingTopicsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/sent.json": { + "get": { + "description": "Get sent topics", + "operationId": "GetSentTopics", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetSentTopics 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSentTopicsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/spam.json": { + "get": { + "description": "Get spam topics", + "operationId": "GetSpamTopics", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetSpamTopics 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSpamTopicsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/spam/all.json": { + "delete": { + "description": "Empty the spam box. Runs synchronously, so it can take a while on a large mailbox.", + "operationId": "EmptySpam", + "responses": { + "200": { + "description": "EmptySpam 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/trash.json": { + "get": { + "description": "Get trash topics", + "operationId": "GetTrashTopics", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetTrashTopics 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTrashTopicsResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/trash/all.json": { + "delete": { + "description": "Empty the trash. Runs synchronously, so it can take a while on a large mailbox.", + "operationId": "EmptyTrash", + "responses": { + "200": { + "description": "EmptyTrash 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}": { + "get": { + "description": "Get a topic", + "operationId": "GetTopic", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetTopic 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTopicResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/entries": { + "get": { + "description": "Get entries for a topic", + "operationId": "GetTopicEntries", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "GetTopicEntries 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTopicEntriesResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-pagination": { + "style": "link", + "totalCountHeader": "X-Total-Count" + }, + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/moves.json": { + "post": { + "description": "Move a topic to another box.\n\nAnswers 204 without moving anything when the acting user has no posting for the topic.", + "operationId": "MoveTopic", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MoveTopicRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "MoveTopic 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/publication.json": { + "get": { + "description": "Whether a thread is shared with a public link, and the link", + "operationId": "GetTopicPublication", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetTopicPublication 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTopicPublicationResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Publications" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/status/active.json": { + "put": { + "description": "Restore a topic from the trash or the catch-all", + "operationId": "RestoreTopic", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "RestoreTopic 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/status/ham.json": { + "put": { + "description": "Mark a spam topic as ham. Every other spam topic from the same sender is hammed too.", + "operationId": "MarkTopicHam", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "MarkTopicHam 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/status/trashed.json": { + "put": { + "description": "Trash a topic.\n\nA shared topic redirects to the removal confirmation page unless confirm_destroy is set,\nso always pass it when trashing something that might be shared.", + "operationId": "TrashTopic", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "confirm_destroy", + "in": "query", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": false + } + } + ], + "responses": { + "200": { + "description": "TrashTopic 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Topics" + ], + "x-hey-retry": { + "maxAttempts": 2, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + }, + "/topics/{topicId}/workflows/{workflowId}/stagings": { + "patch": { + "description": "Move a staged topic to a workflow stage.", + "operationId": "MoveWorkflowStaging", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MoveWorkflowStagingRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "workflowId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "MoveWorkflowStaging 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Workflows" + ] + }, + "post": { + "description": "Add a topic to a workflow. HEY places it in the first stage.", + "operationId": "CreateWorkflowStaging", + "parameters": [ + { + "name": "topicId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + }, + { + "name": "workflowId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateWorkflowStaging 200 response" + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "422": { + "description": "UnprocessableEntityError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Workflows" + ] + } + }, + "/workflows/{workflowId}": { + "get": { + "description": "A workflow with its stages", + "operationId": "GetWorkflow", + "parameters": [ + { + "name": "workflowId", + "in": "path", + "schema": { + "type": "integer", + "format": "int64" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetWorkflow 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWorkflowResponseContent" + } + } + } + }, + "401": { + "description": "UnauthorizedError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "InternalServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Workflows" + ], + "x-hey-retry": { + "maxAttempts": 3, + "baseDelayMs": 1000, + "backoff": "exponential", + "retryOn": [ + 429, + 503 + ] + } + } + } + }, + "components": { + "schemas": { + "Account": { + "type": "object", + "description": "Account — a HEY account", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "status": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "trial": { + "type": "boolean" + }, + "trial_ends_on": { + "type": "string", + "x-go-type": "types.Date", + "x-go-type-import": { + "path": "github.com/basecamp/hey-sdk/go/pkg/types" + }, + "x-omitzero": true + }, + "burner": { + "type": "boolean" + }, + "readonly": { + "type": "boolean" + } + }, + "required": [ + "id" + ] + }, + "AddPostingsToBoxGroupRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "box_id": { + "type": "integer", + "format": "int64" + }, + "box_group_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "box_group_id", + "box_id", + "posting_ids" + ] + }, + "Addressed": { + "type": "object", + "description": "Addressed recipients", + "properties": { + "directly": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "copied": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "blindcopied": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + } + } + }, + "AddressedSender": { + "type": "object", + "description": "AddressedSender — sender context", + "properties": { + "directly": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + } + } + }, + "AdvancedSearchFilters": { + "type": "object", + "description": "AdvancedSearchFilters — the options the advanced search refine form offers", + "properties": { + "refine_in": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchFilterItem" + } + }, + "refine_dates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchFilterItem" + } + }, + "refine_labels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchFilterItem" + } + }, + "refine_attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchFilterItem" + } + } + } + }, + "AdvancedSearchResponseContent": { + "$ref": "#/components/schemas/AdvancedSearchResult" + }, + "AdvancedSearchResult": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchMatch" + } + } + }, + "required": [ + "matches" + ] + }, + "AttachedEntry": { + "type": "object", + "description": "AttachedEntry — entry reference on a calendar event", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "type": "string" + }, + "title": { + "type": "string" + }, + "app_url": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "Attendance": { + "type": "object", + "description": "Attendance — calendar event attendee", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "email_address": { + "type": "string" + }, + "status": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "BadRequestErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "Box": { + "type": "object", + "description": "Box — a HEY mailbox", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "type": "string" + }, + "name": { + "type": "string" + }, + "app_url": { + "type": "string" + }, + "url": { + "type": "string" + }, + "signed_stream_name": { + "type": "string" + }, + "posting_changes_url": { + "type": "string" + }, + "updates_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdatesChannel" + } + } + }, + "required": [ + "id", + "kind", + "name" + ] + }, + "BoxGroup": { + "type": "object", + "description": "BoxGroup — a Set Aside group. The API only ever returns the id.", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id" + ] + }, + "BoxGroupsResponse": { + "type": "object", + "description": "BoxGroupsResponse — the wrapper the groups index answers with", + "properties": { + "box_groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BoxGroup" + } + } + } + }, + "BoxShowResponse": { + "type": "object", + "description": "BoxShowResponse — box detail with postings.\nThe API can return fields at root level or nested under a `box` key.\nSDK response decoders normalize the nested variant to flat before decoding.", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "type": "string" + }, + "name": { + "type": "string" + }, + "app_url": { + "type": "string" + }, + "url": { + "type": "string" + }, + "signed_stream_name": { + "type": "string" + }, + "posting_changes_url": { + "type": "string" + }, + "updates_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdatesChannel" + } + }, + "next_history_url": { + "type": "string" + }, + "next_incremental_sync_url": { + "type": "string" + }, + "postings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + } + } + }, + "required": [ + "id", + "kind", + "name" + ] + }, + "BubbleUpSchedule": { + "type": "object", + "description": "BubbleUpSchedule", + "properties": { + "bubble_up_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "surprise_me": { + "type": "boolean" + } + } + }, + "BulkReplyDelivery": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "entries_count": { + "type": "integer", + "format": "int32" + }, + "delayed": { + "type": "boolean", + "description": "True while the send is held open for undo." + }, + "undo_send_url": { + "type": "string", + "description": "Where to POST to call the replies back, present only while delayed." + } + }, + "required": [ + "delayed", + "entries_count", + "id" + ] + }, + "BulkReplyDraft": { + "type": "object", + "description": "The reply as HEY would send it: the prefilled content and the entries it goes to.", + "properties": { + "content": { + "type": "string", + "description": "The prefilled body — the name tag when every thread is on the same account." + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkReplyEntry" + } + } + }, + "required": [ + "content", + "entries" + ] + }, + "BulkReplyEntry": { + "type": "object", + "description": "One thread a bulk reply answers, with the recipients that thread's reply goes to.", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "topic_id": { + "type": "integer", + "format": "int64" + }, + "topic_name": { + "type": "string" + }, + "addressed": { + "$ref": "#/components/schemas/Addressed" + } + }, + "required": [ + "addressed", + "id", + "topic_id", + "topic_name" + ] + }, + "BulkReplyMessagePayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "required": [ + "content" + ] + }, + "BulkReplyRequestContent": { + "type": "object", + "description": "Wire format: {entry_ids: [...], message: {content}}", + "properties": { + "entry_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "message": { + "$ref": "#/components/schemas/BulkReplyMessagePayload" + } + }, + "required": [ + "entry_ids", + "message" + ] + }, + "BulkUpdateClearancesRequestContent": { + "type": "object", + "properties": { + "ids": { + "type": "string" + }, + "status": { + "type": "string" + }, + "spam": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + } + }, + "required": [ + "ids", + "status" + ] + }, + "BulkUpdateClearancesResponseContent": { + "$ref": "#/components/schemas/ClearanceListResponse" + }, + "Calendar": { + "type": "object", + "description": "Calendar", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "owned": { + "type": "boolean" + }, + "color": { + "type": "string" + }, + "personal": { + "type": "boolean" + }, + "external": { + "type": "boolean" + }, + "url": { + "type": "string" + }, + "recordings_url": { + "type": "string" + }, + "occurrences_url": { + "type": "string" + }, + "owner_email_address": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "CalendarDayListPayload": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CalendarPeriod" + } + } + }, + "required": [ + "days" + ] + }, + "CalendarListPayload": { + "type": "object", + "description": "CalendarListPayload", + "properties": { + "calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CalendarWithRecordingChangesUrl" + } + }, + "calendar_changes_url": { + "type": "string" + }, + "selected_calendar_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "description": "The calendars every period read is drawn from. ToggleCalendar changes this and\nanswers the new one, so a client reads it here once — to open on what is already\non — and takes it from the toggle after that." + } + } + }, + "CalendarPeriod": { + "type": "object", + "description": "CalendarPeriod — a day or a week: its bounds and everything in it, grouped by type.\nRecurring events arrive expanded into the occurrences that fall inside the window,\nwhich is what makes this a different answer than the recordings a calendar lists.", + "properties": { + "starts_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + } + }, + "ends_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + } + }, + "kind": { + "type": "string", + "description": "\"day\" or \"week\"" + }, + "recordings": { + "$ref": "#/components/schemas/CalendarRecordingsResponse" + } + }, + "required": [ + "ends_at", + "kind", + "recordings", + "starts_at" + ] + }, + "CalendarRecordingsResponse": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Recording" + } + }, + "propertyNames": { + "type": "string" + }, + "description": "CalendarRecordingsResponse — recordings grouped by type" + }, + "CalendarSelection": { + "type": "object", + "description": "CalendarSelection — the calendars a toggle left switched on", + "properties": { + "selected_calendar_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "required": [ + "selected_calendar_ids" + ] + }, + "CalendarTodoChanges": { + "type": "object", + "description": "Nothing here is required: a rename sends a title and leaves the day alone.", + "properties": { + "title": { + "type": "string" + }, + "starts_at": { + "type": "string", + "description": "Date string (YYYY-MM-DD). The day the todo is filed on.", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": false, + "x-omitzero": true + }, + "focused": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + } + } + }, + "CalendarTodoPayload": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "starts_at": { + "type": "string", + "description": "Date string (YYYY-MM-DD). Defaults to today if omitted.", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": false, + "x-omitzero": true + } + }, + "required": [ + "title" + ] + }, + "CalendarWeekListPayload": { + "type": "object", + "properties": { + "weeks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CalendarPeriod" + } + } + }, + "required": [ + "weeks" + ] + }, + "CalendarWithRecordingChangesUrl": { + "type": "object", + "description": "CalendarWithRecordingChangesUrl — wraps calendar with sync URL", + "properties": { + "calendar": { + "$ref": "#/components/schemas/Calendar" + }, + "recording_changes_url": { + "type": "string" + } + } + }, + "CalendarYear": { + "type": "object", + "description": "CalendarYear — the grid a year is drawn as. A year carries one entry per day plus the\nevents that span more than one, not every recording it holds: a year's worth of\nexpanded occurrences is not something a client asks for by opening a year.", + "properties": { + "starts_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + } + }, + "ends_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + } + }, + "kind": { + "type": "string", + "description": "\"year\"" + }, + "padding_days_count": { + "type": "integer", + "description": "Days between the reader's week start and January 1st, so the grid lines up", + "format": "int32" + }, + "days": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CalendarYearDay" + } + }, + "spanned_events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Recording" + }, + "description": "All-day and multi-day events, oldest first" + } + }, + "required": [ + "days", + "ends_at", + "kind", + "padding_days_count", + "spanned_events", + "starts_at" + ] + }, + "CalendarYearDay": { + "type": "object", + "properties": { + "starts_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + } + }, + "backgrounded": { + "type": "boolean" + } + }, + "required": [ + "backgrounded", + "starts_at" + ] + }, + "Clearance": { + "type": "object", + "description": "Clearance — screening status for a contact\n\npetitioner and most_recent_entry are only filled in by the Screener reads. The\ncontact reads answer a clearance with nothing but its id and status.", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "petitioner": { + "$ref": "#/components/schemas/Contact" + }, + "most_recent_entry": { + "$ref": "#/components/schemas/Entry" + } + }, + "required": [ + "id" + ] + }, + "ClearanceListResponse": { + "type": "object", + "description": "ClearanceListResponse — wire format: {clearances: [...]}", + "properties": { + "clearances": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Clearance" + } + } + } + }, + "ClearanceSummary": { + "type": "object", + "description": "ClearanceSummary — the Screener's pending count, and the queue itself when asked for\n\nclearances is only present when the read passes include_clearances. Without it HEY\nanswers the count alone, which is what its own apps sync.", + "properties": { + "pending_clearances_count": { + "type": "integer", + "format": "int32" + }, + "signed_stream_name": { + "type": "string" + }, + "clearances": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Clearance" + } + } + } + }, + "Clip": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "content": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "entry_id": { + "type": "integer", + "format": "int64" + }, + "topic": { + "$ref": "#/components/schemas/ClipTopic" + } + }, + "required": [ + "id" + ] + }, + "ClipTopic": { + "type": "object", + "description": "The topic a clip was taken from", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "app_url": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "Collection": { + "type": "object", + "description": "Collection — email collection/label", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "app_url": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "CollectionPayload": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "summary": { + "type": "string" + } + } + }, + "CollectionWithPostings": { + "type": "object", + "description": "CollectionWithPostings — collection detail with its threads as posting objects", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "app_url": { + "type": "string" + }, + "postings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + } + } + }, + "required": [ + "id" + ] + }, + "CompleteCalendarTodoResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "CompleteHabitResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "ConflictErrorResponseContent": { + "type": "object", + "description": "The request conflicts with current state, e.g. starting a time track while one is\nalready ongoing. Time tracks answer {\"error\": \"...\"}; contact writes answer the\n{\"errors\": [...]} list every other error path uses.", + "properties": { + "error": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "contact_id": { + "type": "integer", + "description": "Contact writes only: the contact that was written -- a create that clashes\nstill creates the contact -- and the contacts already holding the email\naddresses that were sent, so a client can offer the merge the web offers.", + "format": "int64" + }, + "conflicting_contact_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "Contact": { + "type": "object", + "description": "Contact — the identity of someone in HEY", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "name": { + "type": "string" + }, + "email_address": { + "type": "string", + "x-hey-sensitive": { + "category": "pii" + } + }, + "avatar_url": { + "type": "string" + }, + "initials": { + "type": "string" + }, + "avatar_background_color": { + "type": "string" + }, + "contactable_type": { + "type": "string" + }, + "name_tag": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "ContactDetail": { + "type": "object", + "description": "ContactDetail — extended contact with additional show fields", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "name": { + "type": "string" + }, + "email_address": { + "type": "string", + "x-hey-sensitive": { + "category": "pii" + } + }, + "avatar_url": { + "type": "string" + }, + "initials": { + "type": "string" + }, + "avatar_background_color": { + "type": "string" + }, + "contactable_type": { + "type": "string" + }, + "name_tag": { + "type": "string" + }, + "edit_app_url": { + "type": "string" + }, + "clearance": { + "$ref": "#/components/schemas/Clearance" + }, + "aliases": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "entries_title": { + "type": "string", + "description": "The heading HEY gives the thread list, e.g. \"All threads with GitHub\"" + }, + "postings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + }, + "description": "One page of the threads this contact is on, newest first" + } + }, + "required": [ + "id" + ] + }, + "ContactNote": { + "type": "object", + "description": "A contact's private note. Empty strings when there is no note.", + "properties": { + "contact_id": { + "type": "integer", + "format": "int64" + }, + "note": { + "type": "string" + }, + "note_html": { + "type": "string", + "description": "The note as editor HTML, the same markup the web hands Trix." + } + }, + "required": [ + "contact_id", + "note", + "note_html" + ] + }, + "ContactNotePayload": { + "type": "object", + "properties": { + "note": { + "type": "string" + } + }, + "required": [ + "note" + ] + }, + "ContactNoteRequestContent": { + "type": "object", + "description": "Wire format: {contact: {note: \"...\"}}", + "properties": { + "contact": { + "$ref": "#/components/schemas/ContactNotePayload" + } + }, + "required": [ + "contact" + ] + }, + "ContactPayload": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email_address": { + "type": "string", + "x-hey-sensitive": { + "category": "pii" + } + }, + "alias_email_addresses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sending the list replaces it: an address left out stops being an alias." + } + }, + "required": [ + "name" + ] + }, + "ContactRequestContent": { + "type": "object", + "description": "Wire format: {contact: {name, email_address, alias_email_addresses: [...]}}", + "properties": { + "contact": { + "$ref": "#/components/schemas/ContactPayload" + } + }, + "required": [ + "contact" + ] + }, + "CreateBoxDesignationRequestContent": { + "type": "object", + "properties": { + "contact_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "contact_id" + ] + }, + "CreateBoxGroupRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "required": [ + "posting_ids" + ] + }, + "CreateBoxGroupResponseContent": { + "$ref": "#/components/schemas/BoxGroup" + }, + "CreateBulkReplyResponseContent": { + "$ref": "#/components/schemas/BulkReplyDelivery" + }, + "CreateCalendarTodoRequestContent": { + "type": "object", + "description": "Wire format: {calendar_todo: {title, starts_at}}", + "properties": { + "calendar_todo": { + "$ref": "#/components/schemas/CalendarTodoPayload" + } + }, + "required": [ + "calendar_todo" + ] + }, + "CreateCalendarTodoResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "CreateContactRequestContent": { + "type": "object", + "description": "Wire format: {acting_user_id, contact: {...}} — creating also has to say which account\nthe contact belongs to, since one identity can hold several.", + "properties": { + "acting_user_id": { + "type": "integer", + "description": "The identity's user on the account the contact should be filed under; Identity's\nall_users carries one per account. Left out, HEY files it under the first account.", + "format": "int64" + }, + "contact": { + "$ref": "#/components/schemas/ContactPayload" + } + }, + "required": [ + "contact" + ] + }, + "CreateContactResponseContent": { + "$ref": "#/components/schemas/Contact" + }, + "CreateDirectUploadRequestContent": { + "type": "object", + "properties": { + "blob": { + "$ref": "#/components/schemas/DirectUploadBlob" + } + }, + "required": [ + "blob" + ] + }, + "CreateFolderForPostingsRequestContent": { + "type": "object", + "description": "Wire format: {posting_ids: [...], folder: {name, status}}", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "folder": { + "$ref": "#/components/schemas/FolderPayload" + } + }, + "required": [ + "folder", + "posting_ids" + ] + }, + "CreateHabitResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "CreateMessageRequestContent": { + "type": "object", + "description": "Wire format: {acting_sender_id, message: {subject, content}, entry: {addressed: {directly: \"...\"}}}", + "properties": { + "acting_sender_id": { + "type": "integer", + "format": "int64" + }, + "message": { + "$ref": "#/components/schemas/MessagePayload" + }, + "entry": { + "$ref": "#/components/schemas/MessageEntryPayload", + "x-go-type-skip-optional-pointer": false + } + }, + "required": [ + "acting_sender_id", + "message" + ] + }, + "CreateReplyRequestContent": { + "type": "object", + "description": "Wire format: {acting_sender_id, message: {content}, entry: {addressed: {directly: [...]}}}\nentry.addressed is optional on the wire but a reply posted without it is saved as a\ndraft rather than delivered — HEY does not reply-all for the caller. Resolve the\nthread's recipients first and always send them.", + "properties": { + "acting_sender_id": { + "type": "integer", + "format": "int64" + }, + "message": { + "$ref": "#/components/schemas/ReplyMessagePayload" + }, + "entry": { + "$ref": "#/components/schemas/MessageEntryPayload", + "x-go-type-skip-optional-pointer": false + } + }, + "required": [ + "acting_sender_id", + "message" + ] + }, + "CreateStickyResponseContent": { + "$ref": "#/components/schemas/Sticky" + }, + "CreateTimeTrackResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "DeletedPosting": { + "type": "object", + "description": "DeletedPosting — the stub the changes feed answers with for a posting that is gone", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "box_id": { + "type": "integer", + "format": "int64" + }, + "deleted_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + } + }, + "required": [ + "id" + ] + }, + "DirectUpload": { + "type": "object", + "properties": { + "signed_id": { + "type": "string" + }, + "attachable_sgid": { + "type": "string" + }, + "direct_upload": { + "$ref": "#/components/schemas/DirectUploadTarget" + } + }, + "required": [ + "attachable_sgid", + "direct_upload", + "signed_id" + ] + }, + "DirectUploadBlob": { + "type": "object", + "properties": { + "filename": { + "type": "string" + }, + "byte_size": { + "type": "integer", + "format": "int64" + }, + "checksum": { + "type": "string" + }, + "content_type": { + "type": "string" + } + }, + "required": [ + "byte_size", + "checksum", + "content_type", + "filename" + ] + }, + "DirectUploadHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "type": "string" + } + }, + "DirectUploadTarget": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "headers": { + "$ref": "#/components/schemas/DirectUploadHeaders" + } + }, + "required": [ + "url" + ] + }, + "Domain": { + "type": "object", + "description": "Domain — email domain", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "address": { + "type": "string" + }, + "app_url": { + "type": "string" + }, + "avatar_url": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "DraftMessage": { + "type": "object", + "description": "DraftMessage — a draft entry", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "subject": { + "type": "string" + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "summary": { + "type": "string" + }, + "url": { + "type": "string" + }, + "app_url": { + "type": "string" + }, + "edit_url": { + "type": "string" + }, + "addressed_contacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "scheduled_delivery_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + } + }, + "required": [ + "id" + ] + }, + "Entry": { + "type": "object", + "description": "Entry — a message entry within a topic", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "alternative_sender_name": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "app_url": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "topic_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id" + ] + }, + "Extenzion": { + "type": "object", + "description": "Extenzion — external account extension", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "app_url": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "ExternalAccount": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "contact": { + "$ref": "#/components/schemas/Contact" + } + }, + "required": [ + "id" + ] + }, + "FilePostingsRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "folder_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "folder_id", + "posting_ids" + ] + }, + "FirstWeekDayParams": { + "type": "object", + "properties": { + "first_week_day": { + "type": "string", + "description": "Lowercase day name, sunday through saturday." + } + }, + "required": [ + "first_week_day" + ] + }, + "FirstWeekDayPreference": { + "type": "object", + "properties": { + "first_week_day": { + "type": "integer", + "description": "0 is Sunday through 6 Saturday, as GetIdentity serves it.", + "format": "int32" + } + }, + "required": [ + "first_week_day" + ] + }, + "Folder": { + "type": "object", + "description": "Folder — email folder", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "app_url": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "FolderPayload": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "FolderWithPostings": { + "type": "object", + "description": "FolderWithPostings — folder detail with the postings filed in it", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "app_url": { + "type": "string" + }, + "postings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + } + } + }, + "required": [ + "id" + ] + }, + "ForbiddenErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "GetAdvancedSearchFiltersResponseContent": { + "$ref": "#/components/schemas/AdvancedSearchFilters" + }, + "GetAsideboxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetBoxPostingChangesResponseContent": { + "type": "object", + "properties": { + "added": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + } + }, + "updated": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + } + }, + "deleted": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeletedPosting" + } + } + } + }, + "GetBoxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetBubbleboxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetBundleUnseenPostingsResponseContent": { + "type": "object", + "properties": { + "contact": { + "$ref": "#/components/schemas/Contact" + }, + "postings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Posting" + } + } + }, + "required": [ + "contact", + "postings" + ] + }, + "GetCalendarDayResponseContent": { + "$ref": "#/components/schemas/CalendarPeriod" + }, + "GetCalendarRecordingsResponseContent": { + "$ref": "#/components/schemas/CalendarRecordingsResponse" + }, + "GetCalendarWeekResponseContent": { + "$ref": "#/components/schemas/CalendarPeriod" + }, + "GetCalendarYearResponseContent": { + "$ref": "#/components/schemas/CalendarYear" + }, + "GetClearancesResponseContent": { + "$ref": "#/components/schemas/ClearanceSummary" + }, + "GetCollectionResponseContent": { + "$ref": "#/components/schemas/CollectionWithPostings" + }, + "GetContactNoteResponseContent": { + "$ref": "#/components/schemas/ContactNote" + }, + "GetContactResponseContent": { + "$ref": "#/components/schemas/ContactDetail" + }, + "GetEverythingTopicsResponseContent": { + "$ref": "#/components/schemas/TopicListResponse" + }, + "GetFeedboxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetFolderResponseContent": { + "$ref": "#/components/schemas/FolderWithPostings" + }, + "GetIdentityResponseContent": { + "$ref": "#/components/schemas/Identity" + }, + "GetImboxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetImboxSeenResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetJournalEntryResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "GetLaterboxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetMessageEditResponseContent": { + "$ref": "#/components/schemas/MessageEditState" + }, + "GetMessageResponseContent": { + "$ref": "#/components/schemas/Message" + }, + "GetMyClearancesResponseContent": { + "$ref": "#/components/schemas/ClearanceListResponse" + }, + "GetNavigationResponseContent": { + "$ref": "#/components/schemas/NavigationResponse" + }, + "GetOngoingTimeTrackResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "GetSentTopicsResponseContent": { + "$ref": "#/components/schemas/TopicListResponse" + }, + "GetSpamTopicsResponseContent": { + "$ref": "#/components/schemas/TopicListResponse" + }, + "GetTopicEntriesResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entry" + } + }, + "GetTopicPublicationResponseContent": { + "$ref": "#/components/schemas/TopicPublication" + }, + "GetTopicResponseContent": { + "$ref": "#/components/schemas/Topic" + }, + "GetTrailboxResponseContent": { + "$ref": "#/components/schemas/BoxShowResponse" + }, + "GetTrashTopicsResponseContent": { + "$ref": "#/components/schemas/TopicListResponse" + }, + "GetWorkflowResponseContent": { + "$ref": "#/components/schemas/Workflow" + }, + "HabitPayload": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "color": { + "type": "string" + }, + "days": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "description": "Days of the week the habit runs on, 0 for Sunday through 6 for Saturday" + } + } + }, + "HabitRequestContent": { + "type": "object", + "description": "Wire format: {calendar_habit: {name, icon, color, days: [0..6]}}", + "properties": { + "calendar_habit": { + "$ref": "#/components/schemas/HabitPayload" + } + }, + "required": [ + "calendar_habit" + ] + }, + "Identity": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "time_zone": { + "type": "string" + }, + "time_zone_name": { + "type": "string" + }, + "time_zone_offset": { + "type": "integer", + "format": "int32" + }, + "auto_time_zone": { + "type": "boolean" + }, + "first_week_day": { + "type": "integer", + "format": "int32" + }, + "time_format": { + "type": "string" + }, + "primary_contact": { + "$ref": "#/components/schemas/Contact" + }, + "all_users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "accounts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Account" + } + }, + "senders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sender" + } + } + }, + "required": [ + "id" + ] + }, + "InternalServerErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "JoinLink": { + "type": "object", + "description": "JoinLink — video/meeting join link", + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "JournalEntryPayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "required": [ + "content" + ] + }, + "ListBoxGroupsResponseContent": { + "$ref": "#/components/schemas/BoxGroupsResponse" + }, + "ListBoxesResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Box" + } + }, + "ListCalendarDaysResponseContent": { + "$ref": "#/components/schemas/CalendarDayListPayload" + }, + "ListCalendarWeeksResponseContent": { + "$ref": "#/components/schemas/CalendarWeekListPayload" + }, + "ListCalendarsResponseContent": { + "$ref": "#/components/schemas/CalendarListPayload" + }, + "ListClipsResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Clip" + } + }, + "ListCollectionsResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collection" + } + }, + "ListContactsResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "ListDraftsResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DraftMessage" + } + }, + "ListJournalEntriesResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Recording" + } + }, + "ListSnippetsResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Snippet" + } + }, + "ListStickiesResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sticky" + } + }, + "ListTimeTrackCategoriesResponseContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeTrackCategory" + } + }, + "ListTimeTracksResponseContent": { + "$ref": "#/components/schemas/TrackedTime" + }, + "MarkPostingsRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "required": [ + "posting_ids" + ] + }, + "Message": { + "type": "object", + "description": "Message — full message detail", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "url": { + "type": "string" + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "sender": { + "$ref": "#/components/schemas/Contact" + }, + "is_reply": { + "type": "boolean" + }, + "subject": { + "type": "string" + }, + "content": { + "type": "string" + }, + "addressed": { + "$ref": "#/components/schemas/Addressed" + }, + "show_addressed_selector": { + "type": "boolean" + }, + "scheduled_delivery_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "posting": { + "$ref": "#/components/schemas/MessagePostingContext" + }, + "addressed_sender": { + "$ref": "#/components/schemas/AddressedSender" + } + }, + "required": [ + "id" + ] + }, + "MessageAddressed": { + "type": "object", + "description": "Recipients per kind, each a list of email addresses.\nhaystack applies Array() to each kind, so a JSON array is the correct wire format\n(a bare string would be treated as a single address, not split on commas).", + "properties": { + "directly": { + "type": "array", + "items": { + "type": "string" + } + }, + "copied": { + "type": "array", + "items": { + "type": "string" + } + }, + "blindcopied": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "MessageDraft": { + "type": "object", + "description": "MessageDraft — a prefilled compose payload (forward, reply). Unsent, so it has no id.", + "properties": { + "url": { + "type": "string" + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "sender": { + "$ref": "#/components/schemas/Contact" + }, + "is_reply": { + "type": "boolean" + }, + "subject": { + "type": "string" + }, + "content": { + "type": "string" + }, + "addressed": { + "$ref": "#/components/schemas/Addressed" + }, + "show_addressed_selector": { + "type": "boolean" + } + } + }, + "MessageEditState": { + "type": "object", + "description": "MessageEditState — a saved draft as the editor sees it. The same compose fields as\nMessageDraft, plus the identity and scheduling a saved entry carries.", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "url": { + "type": "string" + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "sender": { + "$ref": "#/components/schemas/Contact" + }, + "is_reply": { + "type": "boolean" + }, + "subject": { + "type": "string" + }, + "content": { + "type": "string" + }, + "addressed": { + "$ref": "#/components/schemas/Addressed" + }, + "show_addressed_selector": { + "type": "boolean" + }, + "scheduled_delivery_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "posting": { + "$ref": "#/components/schemas/MessagePostingContext" + }, + "addressed_sender": { + "$ref": "#/components/schemas/AddressedSender" + } + }, + "required": [ + "id" + ] + }, + "MessageEntryPayload": { + "type": "object", + "properties": { + "addressed": { + "$ref": "#/components/schemas/MessageAddressed", + "x-go-type-skip-optional-pointer": false + }, + "status": { + "type": "string", + "description": "\"drafted\" saves the entry as a draft instead of delivering it. Any other value\n(or omitting it) delivers through the undo-delay window." + }, + "scheduled_delivery": { + "type": "string", + "description": "\"true\" schedules delivery for the date and hour below; the entry stays drafted\nwith a scheduled_delivery_at until then. On an update, omitting it clears an\nexisting scheduled delivery." + }, + "scheduled_delivery_at_date": { + "type": "string", + "description": "The delivery date: YYYY-MM-DD, \"today\" or \"tomorrow\", read in the identity's\ntime zone." + }, + "scheduled_delivery_at_hour": { + "type": "string", + "description": "The delivery hour, \"0\" through \"23\" — a string so that midnight survives\nomitempty. HEY schedules to the hour." + } + } + }, + "MessagePayload": { + "type": "object", + "properties": { + "subject": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": [ + "content", + "subject" + ] + }, + "MessagePostingContext": { + "type": "object", + "description": "MessagePostingContext — posting context for a message", + "properties": { + "box": { + "type": "string" + } + } + }, + "MovePostingsRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "box_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "box_id", + "posting_ids" + ] + }, + "MoveStickyRequestContent": { + "type": "object", + "description": "Wire format: {id, position} — both at the top level.", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "position": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "position" + ] + }, + "MoveTopicRequestContent": { + "type": "object", + "properties": { + "box_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "box_id" + ] + }, + "MoveWorkflowStagingRequestContent": { + "type": "object", + "properties": { + "workflow_staging": { + "$ref": "#/components/schemas/WorkflowStagingPayload" + } + }, + "required": [ + "workflow_staging" + ] + }, + "NavigationIcon": { + "type": "object", + "description": "NavigationIcon", + "properties": { + "name": { + "type": "string" + }, + "android_url": { + "type": "string" + }, + "ios_url": { + "type": "string" + } + } + }, + "NavigationItem": { + "type": "object", + "description": "NavigationItem", + "properties": { + "title": { + "type": "string" + }, + "app_url": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "hotkey": { + "type": "string" + }, + "highlighted": { + "type": "boolean" + }, + "icon": { + "$ref": "#/components/schemas/NavigationIcon" + }, + "menu_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NavigationItem" + } + } + } + }, + "NavigationResponse": { + "type": "object", + "description": "NavigationResponse", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NavigationItem" + } + }, + "hotkeys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NavigationItem" + } + } + } + }, + "NewBulkReplyResponseContent": { + "$ref": "#/components/schemas/BulkReplyDraft" + }, + "NewEntryForwardResponseContent": { + "$ref": "#/components/schemas/MessageDraft" + }, + "NewEntryReplyResponseContent": { + "$ref": "#/components/schemas/MessageDraft" + }, + "NotFoundErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "Organizer": { + "type": "object", + "description": "Organizer — calendar event organizer", + "properties": { + "email_address": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "Posting": { + "type": "object", + "description": "Posting — polymorphic by `kind` (topic, bundle, entry)", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "observed_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "active_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "box_id": { + "type": "integer", + "format": "int64" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "kind": { + "type": "string", + "description": "Discriminator: \"topic\", \"bundle\", or \"entry\"" + }, + "seen": { + "type": "boolean" + }, + "bundled": { + "type": "boolean" + }, + "muted": { + "type": "boolean" + }, + "note": { + "$ref": "#/components/schemas/PostingNote" + }, + "preapproved_clearance": { + "type": "boolean" + }, + "box_group_id": { + "type": "integer", + "format": "int64" + }, + "includes_attachments": { + "type": "boolean" + }, + "includes_calendar_invites": { + "type": "boolean" + }, + "bubbled_up": { + "type": "boolean" + }, + "bubble_up_waiting_on": { + "type": "boolean" + }, + "bubble_up_schedule": { + "$ref": "#/components/schemas/BubbleUpSchedule" + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "app_url": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "alternative_sender_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "blocked_trackers": { + "type": "boolean" + }, + "contacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "extenzions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extenzion" + } + }, + "folders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Folder" + } + }, + "collections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collection" + } + }, + "workflows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Workflow" + } + }, + "visible_entry_count": { + "type": "integer", + "format": "int32" + }, + "entry_kind": { + "type": "string" + }, + "addressed_contacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "app_bundle_url": { + "type": "string" + } + }, + "required": [ + "id", + "kind" + ], + "x-hey-polymorphic": { + "discriminator": "kind", + "variants": { + "topic": [ + "name", + "blocked_trackers", + "contacts", + "extenzions", + "folders", + "collections", + "workflows", + "visible_entry_count" + ], + "bundle": [ + "name", + "blocked_trackers", + "app_bundle_url" + ], + "entry": [ + "entry_kind", + "addressed_contacts" + ] + } + } + }, + "PostingNote": { + "type": "object", + "description": "Note — a posting note", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "content": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "Recording": { + "type": "object", + "description": "Recording — polymorphic by `type` (CalendarEvent, CalendarTodo, etc.)", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "parent_id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "all_day": { + "type": "boolean" + }, + "recurring": { + "type": "boolean" + }, + "starts_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "ends_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "type": { + "type": "string", + "description": "Discriminator: CalendarEvent, CalendarTodo, etc." + }, + "parent": { + "$ref": "#/components/schemas/Recording" + }, + "starts_at_time_zone": { + "type": "string" + }, + "ends_at_time_zone": { + "type": "string" + }, + "reminders_label": { + "type": "string" + }, + "reminders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reminder" + } + }, + "completed_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "highlighted": { + "type": "boolean" + }, + "recurrence_schedule": { + "$ref": "#/components/schemas/RecurrenceSchedule" + }, + "occurrences_url": { + "type": "string" + }, + "occurrence_id": { + "type": "string" + }, + "calendar": { + "$ref": "#/components/schemas/Calendar" + }, + "edit_url": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "url": { + "type": "string" + }, + "location": { + "type": "string" + }, + "manage_attendance": { + "type": "boolean" + }, + "attendance_status": { + "type": "string" + }, + "organizer": { + "$ref": "#/components/schemas/Organizer" + }, + "attendances": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendance" + } + }, + "attendances_summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "join_link": { + "$ref": "#/components/schemas/JoinLink" + }, + "attached_entry": { + "$ref": "#/components/schemas/AttachedEntry" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string" + }, + "content_html": { + "type": "string", + "description": "Full rich-text HTML of a journal entry (GetJournalEntry / UpdateJournalEntry only;\nlistings carry a truncated plain-text `content` instead)." + }, + "color": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "days": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "icon_url": { + "type": "string" + }, + "stopped_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "notes": { + "type": "string" + }, + "category": { + "type": "string" + }, + "label": { + "type": "string" + }, + "image_url": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "x-hey-polymorphic": { + "discriminator": "type", + "variants": { + "CalendarEvent": [ + "edit_url", + "summary", + "url", + "location", + "manage_attendance", + "attendance_status", + "organizer", + "attendances", + "attendances_summary", + "description", + "join_link", + "attached_entry" + ], + "CalendarTodo": [ + "position" + ], + "CalendarJournalEntry": [ + "content" + ], + "CalendarHabit": [ + "color", + "icon", + "days", + "icon_url", + "stopped_at" + ], + "CalendarTimeTrack": [ + "notes", + "category" + ], + "CalendarCountdown": [ + "label" + ], + "CalendarDayBackground": [ + "image_url" + ] + } + } + }, + "RecurrenceSchedule": { + "type": "object", + "description": "RecurrenceSchedule", + "properties": { + "kind": { + "type": "string" + }, + "description": { + "type": "string" + }, + "preset": { + "type": "boolean" + } + } + }, + "Reminder": { + "type": "object", + "description": "Reminder", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "summary": { + "type": "string" + }, + "duration": { + "type": "integer", + "format": "int32" + }, + "default_duration": { + "type": "boolean" + }, + "iso8601_duration": { + "type": "string" + }, + "delivered": { + "type": "boolean" + }, + "remind_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "label": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "ReplyMessagePayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "required": [ + "content" + ] + }, + "RevealContactResponseContent": { + "$ref": "#/components/schemas/Contact" + }, + "SchedulePostingsBubbleUpRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "slot": { + "type": "string" + }, + "date": { + "type": "string" + } + }, + "required": [ + "posting_ids", + "slot" + ] + }, + "SearchFilterItem": { + "type": "object", + "description": "SearchFilterItem — one option offered by the advanced search refine form", + "properties": { + "title": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "SearchMatch": { + "type": "object", + "description": "One matching topic: the topic, your posting of it (if any), and the entries that matched.", + "properties": { + "topic": { + "$ref": "#/components/schemas/Topic" + }, + "posting_id": { + "type": "integer", + "format": "int64" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entry" + } + } + }, + "required": [ + "topic" + ] + }, + "Sender": { + "type": "object", + "description": "Sender — a contact with default flag", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "email_address": { + "type": "string", + "x-hey-sensitive": { + "category": "pii" + } + }, + "avatar_url": { + "type": "string" + }, + "initials": { + "type": "string" + }, + "avatar_background_color": { + "type": "string" + }, + "contactable_type": { + "type": "string" + }, + "name_tag": { + "type": "string" + }, + "default": { + "type": "boolean" + } + }, + "required": [ + "id" + ] + }, + "ServiceUnavailableErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "Snippet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "content": { + "type": "string", + "description": "Plain text" + }, + "content_html": { + "type": "string", + "description": "Rich-text HTML" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + } + }, + "required": [ + "id" + ] + }, + "StartTimeTrackResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "Sticky": { + "type": "object", + "description": "Sticky — a note on the stickies board", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "body": { + "type": "string" + }, + "size": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + } + }, + "required": [ + "id" + ] + }, + "StickyPayload": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "size": { + "type": "string" + } + } + }, + "StickyRequestContent": { + "type": "object", + "description": "Wire format: {sticky: {body, size}}. Size is \"small\", \"medium\" or \"large\".", + "properties": { + "sticky": { + "$ref": "#/components/schemas/StickyPayload" + } + }, + "required": [ + "sticky" + ] + }, + "TimeFormatPreference": { + "type": "object", + "properties": { + "time_format": { + "type": "string", + "description": "\"twelve_hour\" or \"twenty_four_hour\", as GetIdentity serves it." + } + }, + "required": [ + "time_format" + ] + }, + "TimeTrackCategory": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + } + }, + "required": [ + "id" + ] + }, + "TimeTrackRequestContent": { + "type": "object", + "properties": { + "starts_at": { + "type": "string", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": false + }, + "ends_at": { + "type": "string", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": false + }, + "category_title": { + "type": "string" + }, + "notes": { + "type": "string" + } + }, + "required": [ + "ends_at", + "starts_at" + ] + }, + "ToggleCalendarResponseContent": { + "$ref": "#/components/schemas/CalendarSelection" + }, + "Topic": { + "type": "object", + "description": "Topic detail", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "active_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "status": { + "type": "string" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "app_url": { + "type": "string" + }, + "creator": { + "$ref": "#/components/schemas/Contact" + }, + "contacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Contact" + } + }, + "extenzions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Extenzion" + } + }, + "collections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collection" + } + }, + "is_forged_sender": { + "type": "boolean" + }, + "latest_entry": { + "$ref": "#/components/schemas/Entry" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entry" + }, + "description": "The topic's first page of entries (summaries, no bodies). Present on GetTopic; use\nGetTopicEntries for the rest and GetMessage for a body." + } + }, + "required": [ + "id" + ] + }, + "TopicListResponse": { + "type": "object", + "description": "TopicListResponse — wrapped topic list (sent, spam, trash, everything)", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "topics": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Topic" + } + } + } + }, + "TopicPublication": { + "type": "object", + "properties": { + "published": { + "type": "boolean" + }, + "url": { + "type": "string", + "description": "The public link, when published" + } + }, + "required": [ + "published" + ] + }, + "TrackedTime": { + "type": "object", + "description": "The tracked-time index: a page of completed tracks, and every category they can be\nfiled under.", + "properties": { + "time_tracks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Recording" + } + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeTrackCategory" + } + } + } + }, + "TrashPostingsRequestContent": { + "type": "object", + "properties": { + "posting_ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "remove_access": { + "type": "string", + "description": "Omitted, JSON requests default to removing only your own access from shared topics.\n\"false\" trashes them for everyone instead." + } + }, + "required": [ + "posting_ids" + ] + }, + "UnauthorizedErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "UncompleteCalendarTodoResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "UncompleteHabitResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "UnprocessableEntityErrorResponseContent": { + "type": "object", + "description": "The server rejected what was sent. HEY answers {\"errors\": [\"...\"]} — the messages\nthe model itself produced — so a client can show them as they are.", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + } + } + }, + "UpdateCalendarTodoRequestContent": { + "type": "object", + "description": "Wire format: {calendar_todo: {title, starts_at, focused}}", + "properties": { + "calendar_todo": { + "$ref": "#/components/schemas/CalendarTodoChanges" + } + }, + "required": [ + "calendar_todo" + ] + }, + "UpdateCalendarTodoResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "UpdateClearanceRequestContent": { + "type": "object", + "description": "Wire format: {status: \"approved\"|\"denied\"} — top level, not nested under a clearance key.", + "properties": { + "status": { + "type": "string" + }, + "designation_box_id": { + "type": "integer", + "format": "int64" + }, + "spam": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + }, + "mark_topics_as_seen": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + } + }, + "required": [ + "status" + ] + }, + "UpdateClearanceResponseContent": { + "$ref": "#/components/schemas/Clearance" + }, + "UpdateCollectionRequestContent": { + "type": "object", + "description": "Wire format: {collection: {name, summary}}", + "properties": { + "collection": { + "$ref": "#/components/schemas/CollectionPayload" + } + }, + "required": [ + "collection" + ] + }, + "UpdateContactClearanceRequestContent": { + "type": "object", + "description": "Wire format: {status: \"approved\"|\"denied\"} — top level, not nested under a clearance key.", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "UpdateContactNoteResponseContent": { + "$ref": "#/components/schemas/ContactNote" + }, + "UpdateContactResponseContent": { + "$ref": "#/components/schemas/Contact" + }, + "UpdateFirstWeekDayRequestContent": { + "type": "object", + "description": "Wire format: {identity_preference: {first_week_day: \"monday\"}}", + "properties": { + "identity_preference": { + "$ref": "#/components/schemas/FirstWeekDayParams" + } + }, + "required": [ + "identity_preference" + ] + }, + "UpdateFirstWeekDayResponseContent": { + "$ref": "#/components/schemas/FirstWeekDayPreference" + }, + "UpdateHabitResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "UpdateJournalEntryRequestContent": { + "type": "object", + "description": "Wire format: {calendar_journal_entry: {content}}", + "properties": { + "calendar_journal_entry": { + "$ref": "#/components/schemas/JournalEntryPayload" + } + }, + "required": [ + "calendar_journal_entry" + ] + }, + "UpdateJournalEntryResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "UpdateMyClearanceRequestContent": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "UpdateMyClearanceResponseContent": { + "$ref": "#/components/schemas/Clearance" + }, + "UpdateStickyResponseContent": { + "$ref": "#/components/schemas/Sticky" + }, + "UpdateTimeFormatRequestContent": { + "type": "object", + "properties": { + "twenty_four_hour_time_format": { + "type": "boolean", + "x-go-type-skip-optional-pointer": false + } + }, + "required": [ + "twenty_four_hour_time_format" + ] + }, + "UpdateTimeFormatResponseContent": { + "$ref": "#/components/schemas/TimeFormatPreference" + }, + "UpdateTimeTrackPayload": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Ignored by the server. A time track's title is the constant \"Time Track\";\nHEY dropped per-track titles in 2023. Kept for compatibility only." + }, + "notes": { + "type": "string" + }, + "category": { + "type": "string", + "description": "Ignored by the server, which reads category_title instead. Kept for\ncompatibility only." + }, + "category_title": { + "type": "string", + "description": "Files the track under this category, creating the category if HEY does not\nhave one by that name. Blank is a no-op, not a way to clear the category:\nonce filed, a track can only be moved to another category, or left where it\nis by deleting the category itself." + }, + "starts_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": false, + "x-omitzero": true + }, + "ends_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-go-type-skip-optional-pointer": false, + "x-omitzero": true + } + } + }, + "UpdateTimeTrackRequestContent": { + "type": "object", + "description": "Wire format: {calendar_time_track: {notes, category_title, starts_at, ends_at}}", + "properties": { + "calendar_time_track": { + "$ref": "#/components/schemas/UpdateTimeTrackPayload" + } + }, + "required": [ + "calendar_time_track" + ] + }, + "UpdateTimeTrackResponseContent": { + "$ref": "#/components/schemas/Recording" + }, + "UpdatesChannel": { + "type": "object", + "description": "UpdatesChannel — streaming channel for a box", + "properties": { + "signed_stream_name": { + "type": "string" + } + } + }, + "User": { + "type": "object", + "description": "User — a user within an account", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "account_id": { + "type": "integer", + "format": "int64" + }, + "account_purpose_icon_url": { + "type": "string" + }, + "contact": { + "$ref": "#/components/schemas/Contact" + }, + "external_accounts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalAccount" + } + }, + "auto_responder": { + "type": "boolean" + } + }, + "required": [ + "id" + ] + }, + "Workflow": { + "type": "object", + "description": "Workflow — email workflow/label", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "updated_at": { + "type": "string", + "description": "ISO 8601 date-time timestamp (overrides restJson1 epoch-seconds default)", + "format": "date-time", + "x-go-type": "time.Time", + "x-go-type-import": { + "path": "time" + }, + "x-omitzero": true + }, + "app_url": { + "type": "string" + }, + "stages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowStage" + }, + "description": "The workflow's stages in position order. Present on GetWorkflow." + } + }, + "required": [ + "id" + ] + }, + "WorkflowStage": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "WorkflowStagingPayload": { + "type": "object", + "properties": { + "workflow_stage_id": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "workflow_stage_id" + ] + } + } + } +} diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go new file mode 100644 index 00000000..c5029090 --- /dev/null +++ b/internal/mcpserver/server.go @@ -0,0 +1,67 @@ +package mcpserver + +import ( + "fmt" + "log/slog" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/basecamp/mcp/gateway" + + "github.com/basecamp/hey-cli/internal/version" +) + +// Name identifies the server in the MCP initialize handshake. The version is +// the CLI's own: `hey mcp` is the CLI serving MCP, not a separate product. +const Name = "hey-cli" + +// Config selects the served tool surface. +type Config struct { + // ReadOnly drops every write action from the catalog and refuses write + // dispatch outright. + ReadOnly bool + // Domains narrows the served domains by key ("boxes", "search", ...). + // Empty means all. Unknown keys are a startup error — fail closed. + Domains []string +} + +// Server wraps the toolkit gateway serving hey's derived catalog, dispatching +// through the CLI's authenticated SDK client. +type Server struct { + gw *gateway.Server +} + +// New derives the catalog and hands it to the gateway, which applies the +// config's domain and read-only filters. Tool calls dispatch through api. +func New(api API, cfg Config) (*Server, error) { + if api == nil { + return nil, fmt.Errorf("mcpserver: API client is required") + } + + cat, err := loadCatalog() + if err != nil { + return nil, fmt.Errorf("derive catalog: %w", err) + } + + gw, err := gateway.New(cat.GatewayDomains(), gateway.Config{ + ReadOnly: cfg.ReadOnly, + Domains: cfg.Domains, + Handler: dispatcher{api: api}.handle, + }) + if err != nil { + return nil, err + } + + return &Server{gw: gw}, nil +} + +// Domains returns the served domains. +func (s *Server) Domains() []gateway.Domain { + return s.gw.Domains() +} + +// BuildMCPServer constructs the SDK MCP server with one gateway tool per +// served domain. +func (s *Server) BuildMCPServer(logger *slog.Logger) *mcp.Server { + return s.gw.BuildMCPServer(&mcp.Implementation{Name: Name, Version: version.Version}, logger) +} diff --git a/internal/mcpserver/server_test.go b/internal/mcpserver/server_test.go new file mode 100644 index 00000000..26745cfd --- /dev/null +++ b/internal/mcpserver/server_test.go @@ -0,0 +1,144 @@ +package mcpserver + +import ( + "log/slog" + "slices" + "strings" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/basecamp/mcp/catalog" + "github.com/basecamp/mcp/gateway" + "github.com/basecamp/mcp/mcptest" +) + +func mustFind(t *testing.T, d gateway.Domain, action string) gateway.Operation { + t.Helper() + op, ok := d.Find(action) + if !ok { + t.Fatalf("action %q not found in domain %q", action, d.Name()) + } + return op +} + +func textContent(t *testing.T, result *mcp.CallToolResult) string { + t.Helper() + if len(result.Content) == 0 { + t.Fatal("result has no content") + } + text, ok := result.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("first content is %T, want text", result.Content[0]) + } + return text.Text +} + +func domainByKey(t *testing.T, cat *catalog.Catalog, key string) *catalog.Domain { + t.Helper() + for _, d := range cat.Domains { + if d.Key == key { + return d + } + } + t.Fatalf("domain %q not in catalog", key) + return nil +} + +func connect(t *testing.T, api API, cfg Config) (*Server, *mcp.ClientSession) { + t.Helper() + srv, err := New(api, cfg) + if err != nil { + t.Fatal(err) + } + session := mcptest.Connect(t, srv.BuildMCPServer(slog.New(slog.DiscardHandler))) + return srv, session +} + +func TestServerListsGatewayTools(t *testing.T) { + _, session := connect(t, &fakeAPI{}, Config{}) + + tools := mcptest.ListTools(t, session) + for _, name := range []string{"hey_boxes", "hey_search", "hey_threads", "hey_contacts", "hey_todos", "hey_calendar", "hey_identity"} { + if _, ok := tools[name]; !ok { + t.Errorf("missing tool %q", name) + } + } + if len(tools) != 7 { + t.Errorf("tools/list returned %d tools, want 7", len(tools)) + } +} + +func TestServerDescribeServesOperationSchema(t *testing.T) { + _, session := connect(t, &fakeAPI{}, Config{}) + + text, isError := mcptest.CallText(t, session, "hey_boxes", map[string]any{ + "action": "describe", + "params": map[string]any{"action": "get_box"}, + }) + if isError { + t.Fatalf("describe failed: %s", text) + } + if !strings.Contains(text, "/boxes/{boxId}") { + t.Errorf("describe payload missing operation path: %s", text) + } +} + +func TestServerDispatchesToolCallsThroughAPI(t *testing.T) { + api := &fakeAPI{} + _, session := connect(t, api, Config{}) + + text, isError := mcptest.CallText(t, session, "hey_boxes", map[string]any{ + "action": "list_boxes", + "params": map[string]any{}, + }) + if isError { + t.Fatalf("list_boxes failed: %s", text) + } + if api.method != "GET" || api.path != "/boxes.json" { + t.Errorf("dispatched %s %s, want GET /boxes.json", api.method, api.path) + } + if text != `{"ok":true}` { + t.Errorf("result = %q", text) + } +} + +func TestServerReadOnlyDropsWriteActions(t *testing.T) { + srv, session := connect(t, &fakeAPI{}, Config{ReadOnly: true}) + + for _, d := range srv.Domains() { + if slices.Contains(d.ActionNames(), "create_box_designation") { + t.Error("read-only server still serves create_box_designation") + } + } + + text, isError := mcptest.CallText(t, session, "hey_boxes", map[string]any{ + "action": "create_box_designation", + "params": map[string]any{"boxId": "1"}, + }) + if !isError { + t.Fatalf("write action succeeded on read-only server: %s", text) + } +} + +func TestServerNarrowsDomains(t *testing.T) { + _, session := connect(t, &fakeAPI{}, Config{Domains: []string{"boxes"}}) + + tools := mcptest.ListTools(t, session) + if len(tools) != 1 { + t.Fatalf("tools = %v, want just hey_boxes", tools) + } + if _, ok := tools["hey_boxes"]; !ok { + t.Fatal("hey_boxes missing") + } + + if _, err := New(&fakeAPI{}, Config{Domains: []string{"bogus"}}); err == nil { + t.Error("unknown domain did not fail closed") + } +} + +func TestServerRequiresAPI(t *testing.T) { + if _, err := New(nil, Config{}); err == nil { + t.Error("nil API did not error") + } +} diff --git a/nix/package.nix b/nix/package.nix index 674ec470..12b27a05 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,7 +18,7 @@ buildGoModule.override { inherit go; } (finalAttrs: { # To update: run `make update-nix-hash` (Docker). It rewrites this quoted # value in place, so keep it a string literal rather than lib.fakeHash. - vendorHash = "sha256-NcBIegczL+UYiHTTVa/fnhiur2lWvUH/+lOzSmmx1Io="; + vendorHash = "sha256-HNpAec6YTVfXMqDF5QaFmu7pA/yrnJrWdLqJXDNzcN8="; subPackages = [ "cmd/hey" ]; diff --git a/scripts/sync-mcp-model.sh b/scripts/sync-mcp-model.sh new file mode 100755 index 00000000..c23d4c6b --- /dev/null +++ b/scripts/sync-mcp-model.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Sync the vendored hey-sdk model snapshot that MCP catalog generation reads. +# +# The catalog derives from hey-sdk's behavior model (per-operation traits: +# readonly, idempotent, pagination, retry) joined with its exported OpenAPI +# spec (operationId, method, path, tags, docs, parameter schemas). Both files +# are build products of hey-sdk's Smithy model, so we vendor a snapshot here +# rather than parse Smithy ourselves: CI stays hermetic and the reviewed diff +# shows exactly which surface changed when the SDK moves. Keep the snapshot in +# lockstep with the hey-sdk version pinned in go.mod. +# +# Usage: scripts/sync-mcp-model.sh [path-to-hey-sdk-checkout] +set -eu + +sdk="${1:-../hey-sdk}" +dest="$(dirname "$0")/../internal/mcpserver/model" + +# Resolve provenance before touching the destination, so a checkout that is +# not a git repo (or otherwise broken) can't leave a torn snapshot behind. +# --dirty marks a checkout with uncommitted changes, so modified model files +# are never recorded as a clean release tag. +commit=$(git -C "$sdk" rev-parse HEAD) +ref=$(git -C "$sdk" describe --tags --always --dirty) + +for f in behavior-model.json openapi.json; do + [ -f "$sdk/$f" ] || { echo "missing $sdk/$f (pass a hey-sdk checkout path)" >&2; exit 1; } +done + +for f in behavior-model.json openapi.json; do + cp "$sdk/$f" "$dest/$f" +done + +cat > "$dest/PROVENANCE.json" <