diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ccdd35..aa4b957 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' @@ -17,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/ark-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -29,7 +31,7 @@ jobs: - name: Set up pnpm uses: pnpm/action-setup@v4 with: - version: '10.27.0' + version: '10.30.1' - name: Bootstrap run: ./scripts/bootstrap @@ -41,7 +43,7 @@ jobs: timeout-minutes: 5 name: build runs-on: ${{ github.repository == 'stainless-sdks/ark-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') permissions: contents: read id-token: write @@ -56,7 +58,7 @@ jobs: - name: Set up pnpm uses: pnpm/action-setup@v4 with: - version: '10.27.0' + version: '10.30.1' - name: Bootstrap run: ./scripts/bootstrap @@ -65,14 +67,18 @@ jobs: run: ./scripts/build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/ark-typescript' + if: |- + github.repository == 'stainless-sdks/ark-typescript' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/ark-typescript' + if: |- + github.repository == 'stainless-sdks/ark-typescript' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} @@ -80,7 +86,9 @@ jobs: run: ./scripts/utils/upload-artifact.sh - name: Upload MCP Server tarball - if: github.repository == 'stainless-sdks/ark-typescript' + if: |- + github.repository == 'stainless-sdks/ark-typescript' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s?subpackage=mcp-server AUTH: ${{ steps.github-oidc.outputs.github_token }} @@ -103,7 +111,7 @@ jobs: - name: Set up pnpm uses: pnpm/action-setup@v4 with: - version: '10.27.0' + version: '10.30.1' - name: Bootstrap run: ./scripts/bootstrap diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 1416190..7956ce9 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -36,13 +36,14 @@ jobs: - name: Publish to NPM run: | - if [ -n "${{ github.event.inputs.path }}" ]; then - PATHS_RELEASED='[\"${{ github.event.inputs.path }}\"]' + if [ -n "$INPUT_PATH" ]; then + PATHS_RELEASED="[\"$INPUT_PATH\"]" else PATHS_RELEASED='[\".\", \"packages/mcp-server\"]' fi pnpm tsn scripts/publish-packages.ts "{ \"paths_released\": \"$PATHS_RELEASED\" }" env: + INPUT_PATH: ${{ github.event.inputs.path }} NPM_TOKEN: ${{ secrets.ARK_NPM_TOKEN || secrets.NPM_TOKEN }} - name: Upload MCP Server DXT GitHub release asset diff --git a/.gitignore b/.gitignore index d62bea5..b7d4f6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log node_modules yarn-error.log codegen.log diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4a403c7..71e95ba 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.1" + ".": "0.20.0" } diff --git a/.stats.yml b/.stats.yml index c8ee129..e530c9b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 58 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-98a90852ffca49f4e26c613afff433b17023ee1f81f38ad38a5dad60a0d09881.yml -openapi_spec_hash: c6fd865dd6995df15cf9e6ada2ae791e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-06c3025bf12b191c3906b28173c9b359e24481dd2839dbf3e6dd0b80c1de3fd6.yml +openapi_spec_hash: d8f8fb1f78579997b6381d64cba4e826 config_hash: b70b11b10fc614f91f1c6f028b40780f diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cff799..8b3c2b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,92 @@ # Changelog +## 0.20.0 (2026-04-10) + +Full Changelog: [v0.19.1...v0.20.0](https://github.com/ArkHQ-io/ark-nodejs/compare/v0.19.1...v0.20.0) + +### Features + +* **api:** add tenantId to send ([6fbdf0b](https://github.com/ArkHQ-io/ark-nodejs/commit/6fbdf0b960675ac848e67eabd1922cbfb0238465)) +* **mcp:** add an option to disable code tool ([ae9fbd3](https://github.com/ArkHQ-io/ark-nodejs/commit/ae9fbd3e0bd339a046c8afd526d8cd3b17b77208)) + + +### Bug Fixes + +* **client:** preserve URL params already embedded in path ([dd91411](https://github.com/ArkHQ-io/ark-nodejs/commit/dd91411c605832068c3f46b9d2415549a70a78bd)) +* **docs/contributing:** correct pnpm link command ([8beea6f](https://github.com/ArkHQ-io/ark-nodejs/commit/8beea6fed9825a6d68eea8e65839c8c5ab3a8eac)) +* **mcp:** initialize SDK lazily to avoid failing the connection on init errors ([2c38bc4](https://github.com/ArkHQ-io/ark-nodejs/commit/2c38bc4fc25dee8203ed29ccb0d5681c0c972195)) +* **mcp:** update prompt ([57b8946](https://github.com/ArkHQ-io/ark-nodejs/commit/57b8946bd4e9ec20153c93306e32be7d0be07d4b)) + + +### Chores + +* **ci:** escape input path in publish-npm workflow ([23585b4](https://github.com/ArkHQ-io/ark-nodejs/commit/23585b41adb458bcc2a5d18dc5e2f63bc6853856)) +* **ci:** skip lint on metadata-only changes ([f4703d1](https://github.com/ArkHQ-io/ark-nodejs/commit/f4703d16a4f11f0ad784f213b523a76013dfb7f9)) +* **ci:** skip uploading artifacts on stainless-internal branches ([8ceeec0](https://github.com/ArkHQ-io/ark-nodejs/commit/8ceeec08ddccecc1f4c23d24ae893d8dc730efbd)) +* **internal:** cache fetch instruction calls in MCP server ([70c6458](https://github.com/ArkHQ-io/ark-nodejs/commit/70c6458f5342c89336dd1c3cf6fb01f270771cd0)) +* **internal:** codegen related update ([14c215a](https://github.com/ArkHQ-io/ark-nodejs/commit/14c215a84811fcf8009405a527621d584ee10a56)) +* **internal:** codegen related update ([b23efba](https://github.com/ArkHQ-io/ark-nodejs/commit/b23efba4958abaa2d955ed899fd1324c77d6f523)) +* **internal:** codegen related update ([0690932](https://github.com/ArkHQ-io/ark-nodejs/commit/06909320868dcf47530bd863fd6814d129962333)) +* **internal:** codegen related update ([1b41255](https://github.com/ArkHQ-io/ark-nodejs/commit/1b4125529fcfebdd7e77722497df02b2ed45eb37)) +* **internal:** codegen related update ([64a16a2](https://github.com/ArkHQ-io/ark-nodejs/commit/64a16a2708d314c4e71fe8bfa4bd7de77dceee18)) +* **internal:** codegen related update ([840cca1](https://github.com/ArkHQ-io/ark-nodejs/commit/840cca102a10ef2d36e49e948066640cd6805592)) +* **internal:** codegen related update ([811048d](https://github.com/ArkHQ-io/ark-nodejs/commit/811048d1af1d16e672afe02b2ae0bfa3182a1c1e)) +* **internal:** codegen related update ([7b2d5af](https://github.com/ArkHQ-io/ark-nodejs/commit/7b2d5af6a5eff12fbe5064a4836da0c9676d22b5)) +* **internal:** codegen related update ([2a78b3d](https://github.com/ArkHQ-io/ark-nodejs/commit/2a78b3dce8bd987bafdae838198d84210ceecc7b)) +* **internal:** codegen related update ([c37d70d](https://github.com/ArkHQ-io/ark-nodejs/commit/c37d70dcdcac915c2ccd876cd9dc98d73fab966f)) +* **internal:** codegen related update ([08e9888](https://github.com/ArkHQ-io/ark-nodejs/commit/08e9888e246c70a645acd5e5278e0f956f935c4e)) +* **internal:** codegen related update ([b335bdb](https://github.com/ArkHQ-io/ark-nodejs/commit/b335bdb55f0cdcf4026b902e82a0e182c76ce015)) +* **internal:** codegen related update ([1ea5bfb](https://github.com/ArkHQ-io/ark-nodejs/commit/1ea5bfb37cec1f3394eba8d9f3392e8dc52fa3ed)) +* **internal:** codegen related update ([f2af2ac](https://github.com/ArkHQ-io/ark-nodejs/commit/f2af2ac4011d120c57df8f52017aafe8cea71198)) +* **internal:** codegen related update ([c49d867](https://github.com/ArkHQ-io/ark-nodejs/commit/c49d86766d9dc9de536bc2283f210ba77d6c0659)) +* **internal:** codegen related update ([f70411d](https://github.com/ArkHQ-io/ark-nodejs/commit/f70411d5c2cfacb0105cec1cfd8071099ad12e54)) +* **internal:** codegen related update ([1271f6f](https://github.com/ArkHQ-io/ark-nodejs/commit/1271f6f6e3f0945d1b4c6f9cdade67386d7a3a67)) +* **internal:** codegen related update ([526c820](https://github.com/ArkHQ-io/ark-nodejs/commit/526c8205065577ffb9e480bbf363b1fad2b7cb36)) +* **internal:** fix MCP Dockerfiles so they can be built without buildkit ([f3e1ae0](https://github.com/ArkHQ-io/ark-nodejs/commit/f3e1ae0b09825a183ab87e279af5e1185aa9138b)) +* **internal:** fix MCP Dockerfiles so they can be built without buildkit ([ae0df75](https://github.com/ArkHQ-io/ark-nodejs/commit/ae0df750c9a801293c8e5554963be30dfd1f8e06)) +* **internal:** fix MCP server import ordering ([e6a2ac3](https://github.com/ArkHQ-io/ark-nodejs/commit/e6a2ac35c31c36ecebc005c6ea9055954558c747)) +* **internal:** fix MCP server TS errors that occur with required client options ([322f87e](https://github.com/ArkHQ-io/ark-nodejs/commit/322f87e3241f0b2ff519830cf5c16e4822d9809d)) +* **internal:** improve local docs search for MCP servers ([f134a98](https://github.com/ArkHQ-io/ark-nodejs/commit/f134a98f96202a7bae902bc2cea2a28dee36a0c9)) +* **internal:** improve local docs search for MCP servers ([6faba1a](https://github.com/ArkHQ-io/ark-nodejs/commit/6faba1a10eaa6f7f15ed1e9362245f833924e871)) +* **internal:** make generated MCP servers compatible with Cloudflare worker environments ([1e31765](https://github.com/ArkHQ-io/ark-nodejs/commit/1e317656f82499cc3807bac65e7d3a18e7e97f1c)) +* **internal:** make MCP code execution location configurable via a flag ([dfdead0](https://github.com/ArkHQ-io/ark-nodejs/commit/dfdead08790f1f37332a96ad0bd8449303eda7b1)) +* **internal:** move stringifyQuery implementation to internal function ([cf98d02](https://github.com/ArkHQ-io/ark-nodejs/commit/cf98d02984384b8a5067912df43a3d473b78ac90)) +* **internal:** show error causes in MCP servers when running in local mode ([274906a](https://github.com/ArkHQ-io/ark-nodejs/commit/274906a4c555bf77ad0f831d3c49ec417ecba3ff)) +* **internal:** support custom-instructions-path flag in MCP servers ([d619e6c](https://github.com/ArkHQ-io/ark-nodejs/commit/d619e6cf7bec1156b01077ae365225c064c1ff0c)) +* **internal:** support local docs search in MCP servers ([d7b6c02](https://github.com/ArkHQ-io/ark-nodejs/commit/d7b6c027b474ee92b754adba631e3dd9876ea4bd)) +* **internal:** support type annotations when running MCP in local execution mode ([5bc40dd](https://github.com/ArkHQ-io/ark-nodejs/commit/5bc40dd0b3d667b40082b8ae951dbb28906d0388)) +* **internal:** support x-stainless-mcp-client-envs header in MCP servers ([3baa9fc](https://github.com/ArkHQ-io/ark-nodejs/commit/3baa9fcc0a0cc5f043663be331e94b1fc63e36cc)) +* **internal:** support x-stainless-mcp-client-permissions headers in MCP servers ([8dfe0cf](https://github.com/ArkHQ-io/ark-nodejs/commit/8dfe0cf9834efb627e6497fca376faff0692e191)) +* **internal:** tweak CI branches ([754549d](https://github.com/ArkHQ-io/ark-nodejs/commit/754549d106a22b74a87b9332c9d6f3f671a8946c)) +* **internal:** update dependencies to address dependabot vulnerabilities ([c4378da](https://github.com/ArkHQ-io/ark-nodejs/commit/c4378da3eef2a11aa6e3fa4b3539b2e48b071818)) +* **internal:** update gitignore ([f9f0bfb](https://github.com/ArkHQ-io/ark-nodejs/commit/f9f0bfbe65c243ea6cbb6162a2234a856b229ab2)) +* **internal:** update lock file ([c927691](https://github.com/ArkHQ-io/ark-nodejs/commit/c927691edff4d1434fe28c1a5b4911882d2417e1)) +* **internal:** update lockfile ([ddb9868](https://github.com/ArkHQ-io/ark-nodejs/commit/ddb986866ce29017b12f937b4234110c075ea1be)) +* **internal:** update multipart form array serialization ([d20bbc4](https://github.com/ArkHQ-io/ark-nodejs/commit/d20bbc41fec80127d858ca5f90a9bca9863a250b)) +* **internal:** upgrade @modelcontextprotocol/sdk and hono ([a62c2db](https://github.com/ArkHQ-io/ark-nodejs/commit/a62c2db41c7f5de400cbb45d4d61821ad6d23eaf)) +* **internal:** upgrade pnpm version ([92d897c](https://github.com/ArkHQ-io/ark-nodejs/commit/92d897ccfcd8bcbfcb66b9c9456a3484fc93813f)) +* **internal:** use x-stainless-mcp-client-envs header for MCP remote code tool calls ([1536aa4](https://github.com/ArkHQ-io/ark-nodejs/commit/1536aa4f6deff7c608203fa4aa4a92500b4bb1c7)) +* **mcp-server:** add support for session id, forward client info ([6dbdb7c](https://github.com/ArkHQ-io/ark-nodejs/commit/6dbdb7ce42f618c34740634523d4b8d31f9286d0)) +* **mcp-server:** improve instructions ([034fe72](https://github.com/ArkHQ-io/ark-nodejs/commit/034fe72954e4d7d6458baebcd466743274719e4d)) +* **mcp-server:** increase local docs search result count from 5 to 10 ([2a702d7](https://github.com/ArkHQ-io/ark-nodejs/commit/2a702d711c44effb1dc4e2d0498f93f5263b2200)) +* **mcp-server:** log client info ([c881a14](https://github.com/ArkHQ-io/ark-nodejs/commit/c881a147b57578239283817a55c8ed20a0018962)) +* **mcp-server:** return access instructions for 404 without API key ([7b95543](https://github.com/ArkHQ-io/ark-nodejs/commit/7b95543cce2bcd6e7485b7b72fb661f83f888534)) +* **mcp:** correctly update version in sync with sdk ([1dadb63](https://github.com/ArkHQ-io/ark-nodejs/commit/1dadb630f2bbd1bad630e10761524a890a1c5b84)) +* **test:** do not count install time for mock server timeout ([7e8fb67](https://github.com/ArkHQ-io/ark-nodejs/commit/7e8fb67a552e618d84b159fc7949716358e5f651)) +* **tests:** bump steady to v0.19.4 ([74bea4d](https://github.com/ArkHQ-io/ark-nodejs/commit/74bea4da408c9109c6f6cfcaff3e64cf3d1598ae)) +* **tests:** bump steady to v0.19.5 ([cf9ec91](https://github.com/ArkHQ-io/ark-nodejs/commit/cf9ec91d28bea56831e54837f3dcb70b0045dc2b)) +* **tests:** bump steady to v0.19.6 ([6635173](https://github.com/ArkHQ-io/ark-nodejs/commit/663517360ee9844c63880294cc2cb1c42e54b9dc)) +* **tests:** bump steady to v0.19.7 ([448907b](https://github.com/ArkHQ-io/ark-nodejs/commit/448907bbfe0b30f7b63781f39dfb197ac00163c7)) +* **tests:** bump steady to v0.20.1 ([8cc3098](https://github.com/ArkHQ-io/ark-nodejs/commit/8cc3098ea24e01ee29de534a6a8f7f4b4de2ae9b)) +* **tests:** bump steady to v0.20.2 ([361bb78](https://github.com/ArkHQ-io/ark-nodejs/commit/361bb78948f15b6e778c4e8728700eb31ae0d5d3)) +* update mock server docs ([2a5b350](https://github.com/ArkHQ-io/ark-nodejs/commit/2a5b35073fbd73679e553f33acd495521420f638)) + + +### Refactors + +* **tests:** switch from prism to steady ([85e7b1a](https://github.com/ArkHQ-io/ark-nodejs/commit/85e7b1a01ed063105884bb59933bb4d788ca642b)) +* update sdk ([baad2b3](https://github.com/ArkHQ-io/ark-nodejs/commit/baad2b38bfd71bccea2d84bad2137efb3c976d64)) + ## 0.19.1 (2026-02-18) Full Changelog: [v0.19.0...v0.19.1](https://github.com/ArkHQ-io/ark-nodejs/compare/v0.19.0...v0.19.1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a72edfd..f8f2e35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,15 +60,15 @@ $ yarn link ark-email # With pnpm $ pnpm link --global $ cd ../my-package -$ pnpm link -—global ark-email +$ pnpm link --global ark-email ``` ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh -$ npx prism mock path/to/your/openapi.yml +$ ./scripts/mock ``` ```sh diff --git a/package.json b/package.json index 2dc7545..059ceeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ark-email", - "version": "0.19.1", + "version": "0.20.0", "description": "The official TypeScript library for the Ark API", "author": "Ark ", "types": "dist/index.d.ts", @@ -8,7 +8,7 @@ "type": "commonjs", "repository": "github:ArkHQ-io/ark-nodejs", "license": "Apache-2.0", - "packageManager": "pnpm@10.27.0", + "packageManager": "pnpm@10.30.1", "files": [ "**/*" ], @@ -50,6 +50,17 @@ "typescript": "5.8.3", "typescript-eslint": "8.31.1" }, + "overrides": { + "minimatch": "^9.0.5" + }, + "pnpm": { + "overrides": { + "minimatch": "^9.0.5" + } + }, + "resolutions": { + "minimatch": "^9.0.5" + }, "exports": { ".": { "import": "./dist/index.mjs", diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 9704fa7..fa0a8d6 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -43,8 +43,12 @@ ENV CI=true RUN pnpm install --frozen-lockfile && \ pnpm build -# Production stage -FROM node:24-alpine +FROM denoland/deno:alpine-2.7.1 + +# Install node and npm +RUN apk add --no-cache nodejs npm + +ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib # Add non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 @@ -60,6 +64,7 @@ COPY --from=builder /build/dist ./node_modules/ark-email # Change ownership to nodejs user RUN chown -R nodejs:nodejs /app +RUN chown -R nodejs:nodejs /deno-dir # Switch to non-root user USER nodejs diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 2596011..eda1212 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,7 +1,7 @@ { "dxt_version": "0.2", "name": "ark-email-mcp", - "version": "0.5.1", + "version": "0.20.0", "description": "The official MCP Server for the Ark API", "author": { "name": "Ark", @@ -18,7 +18,9 @@ "entry_point": "index.js", "mcp_config": { "command": "node", - "args": ["${__dirname}/index.js"], + "args": [ + "${__dirname}/index.js" + ], "env": { "ARK_API_KEY": "${user_config.ARK_API_KEY}" } @@ -39,5 +41,7 @@ "node": ">=18.0.0" } }, - "keywords": ["api"] + "keywords": [ + "api" + ] } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index f8cc58e..c72e443 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "ark-email-mcp", - "version": "0.19.1", + "version": "0.20.0", "description": "The official MCP Server for the Ark API", "author": "Ark ", "types": "dist/index.d.ts", @@ -13,7 +13,7 @@ }, "homepage": "https://github.com/ArkHQ-io/ark-nodejs/tree/main/packages/mcp-server#readme", "license": "Apache-2.0", - "packageManager": "pnpm@10.27.0", + "packageManager": "pnpm@10.30.1", "private": false, "publishConfig": { "access": "public" @@ -25,21 +25,26 @@ "prepublishOnly": "echo 'to publish, run pnpm build && (cd dist; pnpm publish)' && exit 1", "format": "prettier --write --cache --cache-strategy metadata . !dist", "tsn": "ts-node -r tsconfig-paths/register", - "lint": "eslint --ext ts,js .", - "fix": "eslint --fix --ext ts,js ." + "lint": "eslint .", + "fix": "eslint --fix ." }, "dependencies": { "ark-email": "workspace:*", + "ajv": "^8.18.0", "@cloudflare/cabidela": "^0.2.4", - "@modelcontextprotocol/sdk": "^1.25.2", + "@hono/node-server": "^1.19.10", + "@modelcontextprotocol/sdk": "^1.27.1", + "hono": "^4.12.4", "@valtown/deno-http-worker": "^0.0.21", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^5.1.0", "fuse.js": "^7.1.0", + "minisearch": "^7.2.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", - "morgan": "^1.10.0", - "morgan-body": "^2.6.9", + "pino": "^10.3.1", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", @@ -56,14 +61,13 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", - "@types/morgan": "^1.9.10", "@types/qs": "^6.14.0", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", - "eslint": "^8.49.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-unused-imports": "^3.0.0", + "eslint": "^9.39.1", + "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-unused-imports": "^4.1.4", "jest": "^29.4.0", "prettier": "^3.0.0", "ts-jest": "^29.1.0", diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts new file mode 100644 index 0000000..78263e4 --- /dev/null +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -0,0 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export function getWorkerPath(): string { + return require.resolve('./code-tool-worker.mjs'); +} diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts index 8eef065..faa8138 100644 --- a/packages/mcp-server/src/code-tool-types.ts +++ b/packages/mcp-server/src/code-tool-types.ts @@ -8,6 +8,7 @@ export type WorkerInput = { client_opts: ClientOptions; intent?: string | undefined; }; + export type WorkerOutput = { is_error: boolean; result: unknown | null; diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts new file mode 100644 index 0000000..f7db80d --- /dev/null +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -0,0 +1,337 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import path from 'node:path'; +import util from 'node:util'; +import Fuse from 'fuse.js'; +import ts from 'typescript'; +import { WorkerOutput } from './code-tool-types'; +import { Ark, ClientOptions } from 'ark-email'; + +async function tseval(code: string) { + return import('data:application/typescript;charset=utf-8;base64,' + Buffer.from(code).toString('base64')); +} + +function getRunFunctionSource(code: string): { + type: 'declaration' | 'expression'; + client: string | undefined; + code: string; +} | null { + const sourceFile = ts.createSourceFile('code.ts', code, ts.ScriptTarget.Latest, true); + const printer = ts.createPrinter(); + + for (const statement of sourceFile.statements) { + // Check for top-level function declarations + if (ts.isFunctionDeclaration(statement)) { + if (statement.name?.text === 'run') { + return { + type: 'declaration', + client: statement.parameters[0]?.name.getText(), + code: printer.printNode(ts.EmitHint.Unspecified, statement.body!, sourceFile), + }; + } + } + + // Check for variable declarations: const run = () => {} or const run = function() {} + if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + if ( + ts.isIdentifier(declaration.name) && + declaration.name.text === 'run' && + // Check if it's initialized with a function + declaration.initializer && + (ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer)) + ) { + return { + type: 'expression', + client: declaration.initializer.parameters[0]?.name.getText(), + code: printer.printNode(ts.EmitHint.Unspecified, declaration.initializer, sourceFile), + }; + } + } + } + } + + return null; +} + +function getTSDiagnostics(code: string): string[] { + const functionSource = getRunFunctionSource(code)!; + const codeWithImport = [ + 'import { Ark } from "ark-email";', + functionSource.type === 'declaration' ? + `async function run(${functionSource.client}: Ark)` + : `const run: (${functionSource.client}: Ark) => Promise =`, + functionSource.code, + ].join('\n'); + const sourcePath = path.resolve('code.ts'); + const ast = ts.createSourceFile(sourcePath, codeWithImport, ts.ScriptTarget.Latest, true); + const options = ts.getDefaultCompilerOptions(); + options.target = ts.ScriptTarget.Latest; + options.module = ts.ModuleKind.NodeNext; + options.moduleResolution = ts.ModuleResolutionKind.NodeNext; + const host = ts.createCompilerHost(options, true); + const newHost: typeof host = { + ...host, + getSourceFile: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return ast; + } + return host.getSourceFile(...args); + }, + readFile: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return codeWithImport; + } + return host.readFile(...args); + }, + fileExists: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return true; + } + return host.fileExists(...args); + }, + }; + const program = ts.createProgram({ + options, + rootNames: [sourcePath], + host: newHost, + }); + const diagnostics = ts.getPreEmitDiagnostics(program, ast); + return diagnostics.map((d) => { + const message = ts.flattenDiagnosticMessageText(d.messageText, '\n'); + if (!d.file || !d.start) return `- ${message}`; + const { line: lineNumber } = ts.getLineAndCharacterOfPosition(d.file, d.start); + const line = codeWithImport.split('\n').at(lineNumber)?.trim(); + return line ? `- ${message}\n ${line}` : `- ${message}`; + }); +} + +const fuse = new Fuse( + [ + 'client.emails.list', + 'client.emails.retrieve', + 'client.emails.retrieveDeliveries', + 'client.emails.retry', + 'client.emails.send', + 'client.emails.sendBatch', + 'client.emails.sendRaw', + 'client.logs.list', + 'client.logs.retrieve', + 'client.usage.export', + 'client.usage.listTenants', + 'client.usage.retrieve', + 'client.limits.retrieve', + 'client.tenants.create', + 'client.tenants.delete', + 'client.tenants.list', + 'client.tenants.retrieve', + 'client.tenants.update', + 'client.tenants.credentials.create', + 'client.tenants.credentials.delete', + 'client.tenants.credentials.list', + 'client.tenants.credentials.retrieve', + 'client.tenants.credentials.update', + 'client.tenants.domains.create', + 'client.tenants.domains.delete', + 'client.tenants.domains.list', + 'client.tenants.domains.retrieve', + 'client.tenants.domains.verify', + 'client.tenants.suppressions.create', + 'client.tenants.suppressions.delete', + 'client.tenants.suppressions.list', + 'client.tenants.suppressions.retrieve', + 'client.tenants.webhooks.create', + 'client.tenants.webhooks.delete', + 'client.tenants.webhooks.list', + 'client.tenants.webhooks.listDeliveries', + 'client.tenants.webhooks.replayDelivery', + 'client.tenants.webhooks.retrieve', + 'client.tenants.webhooks.retrieveDelivery', + 'client.tenants.webhooks.test', + 'client.tenants.webhooks.update', + 'client.tenants.tracking.create', + 'client.tenants.tracking.delete', + 'client.tenants.tracking.list', + 'client.tenants.tracking.retrieve', + 'client.tenants.tracking.update', + 'client.tenants.tracking.verify', + 'client.tenants.usage.retrieve', + 'client.tenants.usage.retrieveTimeseries', + 'client.platform.webhooks.create', + 'client.platform.webhooks.delete', + 'client.platform.webhooks.list', + 'client.platform.webhooks.listDeliveries', + 'client.platform.webhooks.replayDelivery', + 'client.platform.webhooks.retrieve', + 'client.platform.webhooks.retrieveDelivery', + 'client.platform.webhooks.test', + 'client.platform.webhooks.update', + ], + { threshold: 1, shouldSort: true }, +); + +function getMethodSuggestions(fullyQualifiedMethodName: string): string[] { + return fuse + .search(fullyQualifiedMethodName) + .map(({ item }) => item) + .slice(0, 5); +} + +const proxyToObj = new WeakMap(); +const objToProxy = new WeakMap(); + +type ClientProxyConfig = { + path: string[]; + isBelievedBad?: boolean; +}; + +function makeSdkProxy(obj: T, { path, isBelievedBad = false }: ClientProxyConfig): T { + let proxy: T = objToProxy.get(obj); + + if (!proxy) { + proxy = new Proxy(obj, { + get(target, prop, receiver) { + const propPath = [...path, String(prop)]; + const value = Reflect.get(target, prop, receiver); + + if (isBelievedBad || (!(prop in target) && value === undefined)) { + // If we're accessing a path that doesn't exist, it will probably eventually error. + // Let's proxy it and mark it bad so that we can control the error message. + // We proxy an empty class so that an invocation or construction attempt is possible. + return makeSdkProxy(class {}, { path: propPath, isBelievedBad: true }); + } + + if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + return makeSdkProxy(value, { path: propPath, isBelievedBad }); + } + + return value; + }, + + apply(target, thisArg, args) { + if (isBelievedBad || typeof target !== 'function') { + const fullyQualifiedMethodName = path.join('.'); + const suggestions = getMethodSuggestions(fullyQualifiedMethodName); + throw new Error( + `${fullyQualifiedMethodName} is not a function. Did you mean: ${suggestions.join(', ')}`, + ); + } + + return Reflect.apply(target, proxyToObj.get(thisArg) ?? thisArg, args); + }, + + construct(target, args, newTarget) { + if (isBelievedBad || typeof target !== 'function') { + const fullyQualifiedMethodName = path.join('.'); + const suggestions = getMethodSuggestions(fullyQualifiedMethodName); + throw new Error( + `${fullyQualifiedMethodName} is not a constructor. Did you mean: ${suggestions.join(', ')}`, + ); + } + + return Reflect.construct(target, args, newTarget); + }, + }); + + objToProxy.set(obj, proxy); + proxyToObj.set(proxy, obj); + } + + return proxy; +} + +function parseError(code: string, error: unknown): string | undefined { + if (!(error instanceof Error)) return; + const cause = error.cause instanceof Error ? `: ${error.cause.message}` : ''; + const message = error.name ? `${error.name}: ${error.message}${cause}` : `${error.message}${cause}`; + try { + // Deno uses V8; the first ":LINE:COLUMN" is the top of stack. + const lineNumber = error.stack?.match(/:([0-9]+):[0-9]+/)?.[1]; + // -1 for the zero-based indexing + const line = + lineNumber && + code + .split('\n') + .at(parseInt(lineNumber, 10) - 1) + ?.trim(); + return line ? `${message}\n at line ${lineNumber}\n ${line}` : message; + } catch { + return message; + } +} + +const fetch = async (req: Request): Promise => { + const { opts, code } = (await req.json()) as { opts: ClientOptions; code: string }; + + const runFunctionSource = code ? getRunFunctionSource(code) : null; + if (!runFunctionSource) { + const message = + code ? + 'The code is missing a top-level `run` function.' + : 'The code argument is missing. Provide one containing a top-level `run` function.'; + return Response.json( + { + is_error: true, + result: `${message} Write code within this template:\n\n\`\`\`\nasync function run(client) {\n // Fill this out\n}\n\`\`\``, + log_lines: [], + err_lines: [], + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const diagnostics = getTSDiagnostics(code); + if (diagnostics.length > 0) { + return Response.json( + { + is_error: true, + result: `The code contains TypeScript diagnostics:\n${diagnostics.join('\n')}`, + log_lines: [], + err_lines: [], + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const client = new Ark({ + ...opts, + }); + + const log_lines: string[] = []; + const err_lines: string[] = []; + const originalConsole = globalThis.console; + globalThis.console = { + ...originalConsole, + log: (...args: unknown[]) => { + log_lines.push(util.format(...args)); + }, + error: (...args: unknown[]) => { + err_lines.push(util.format(...args)); + }, + }; + try { + let run_ = async (client: any) => {}; + run_ = (await tseval(`${code}\nexport default run;`)).default; + const result = await run_(makeSdkProxy(client, { path: ['client'] })); + return Response.json({ + is_error: false, + result, + log_lines, + err_lines, + } satisfies WorkerOutput); + } catch (e) { + return Response.json( + { + is_error: true, + result: parseError(code, e), + log_lines, + err_lines, + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } finally { + globalThis.console = originalConsole; + } +}; + +export default { fetch }; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index c6b1fec..1921c53 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { + ContentBlock, McpRequestContext, McpTool, Metadata, @@ -11,11 +12,14 @@ import { import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { getLogger } from './logger'; import { SdkMethod } from './methods'; +import { McpCodeExecutionMode } from './options'; +import { ClientOptions } from 'ark-email'; const prompt = `Runs JavaScript code to interact with the Ark API. -You are a skilled programmer writing code to interface with the service. +You are a skilled TypeScript programmer writing code to interface with the service. Define an async function named "run" that takes a single parameter of an initialized SDK client and it will be run. For example: @@ -38,7 +42,9 @@ You will be returned anything that your function returns, plus the results of an Do not add try-catch blocks for single API calls. The tool will handle errors for you. Do not add comments unless necessary for generating better code. Code will run in a container, and cannot interact with the network outside of the given SDK client. -Variables will not persist between calls, so make sure to return or log any data you might need later.`; +Variables will not persist between calls, so make sure to return or log any data you might need later. +Remember that you are writing TypeScript code, so you need to be careful with your types. +Always type dynamic key-value stores explicitly as Record instead of {}.`; /** * A tool that runs code against a copy of the SDK. @@ -47,9 +53,19 @@ Variables will not persist between calls, so make sure to return or log any data * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then * a generic endpoint that can be used to invoke any endpoint with the provided arguments. * - * @param endpoints - The endpoints to include in the list. + * @param blockedMethods - The methods to block for code execution. Blocking is done by simple string + * matching, so it is not secure against obfuscation. For stronger security, block in the downstream API + * with limited API keys. + * @param codeExecutionMode - Whether to execute code in a local Deno environment or in a remote + * sandbox environment hosted by Stainless. */ -export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { +export function codeTool({ + blockedMethods, + codeExecutionMode, +}: { + blockedMethods: SdkMethod[] | undefined; + codeExecutionMode: McpCodeExecutionMode; +}): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -69,6 +85,9 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und required: ['code'], }, }; + + const logger = getLogger(); + const handler = async ({ reqContext, args, @@ -77,9 +96,6 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und args: any; }): Promise => { const code = args.code as string; - const intent = args.intent as string | undefined; - const client = reqContext.client; - // Do very basic blocking of code that includes forbidden method names. // // WARNING: This is not secure against obfuscation and other evasion methods. If @@ -96,51 +112,290 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und } } - const codeModeEndpoint = - readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; - - // Setting a Stainless API key authenticates requests to the code tool endpoint. - const res = await fetch(codeModeEndpoint, { - method: 'POST', - headers: { - ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), - 'Content-Type': 'application/json', - client_envs: JSON.stringify({ - ARK_API_KEY: requireValue( - readEnv('ARK_API_KEY') ?? client.apiKey, - 'set ARK_API_KEY environment variable or provide apiKey client option', - ), - ARK_BASE_URL: readEnv('ARK_BASE_URL') ?? client.baseURL ?? undefined, - }), + let result: ToolCallResult; + const startTime = Date.now(); + + if (codeExecutionMode === 'local') { + logger.debug('Executing code in local Deno environment'); + result = await localDenoHandler({ reqContext, args }); + } else { + logger.debug('Executing code in remote Stainless environment'); + result = await remoteStainlessHandler({ reqContext, args }); + } + + logger.info( + { + codeExecutionMode, + durationMs: Date.now() - startTime, + isError: result.isError, + contentRows: result.content?.length ?? 0, }, - body: JSON.stringify({ - project_name: 'ark', - code, - intent, - client_opts: {}, - } satisfies WorkerInput), - }); + 'Got code tool execution result', + ); + return result; + }; + + return { metadata, tool, handler }; +} + +const remoteStainlessHandler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: any; +}): Promise => { + const code = args.code as string; + const intent = args.intent as string | undefined; + const client = reqContext.client; - if (!res.ok) { + const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + + const localClientEnvs = { + ARK_API_KEY: requireValue( + readEnv('ARK_API_KEY') ?? client.apiKey, + 'set ARK_API_KEY environment variable or provide apiKey client option', + ), + ARK_BASE_URL: readEnv('ARK_BASE_URL') ?? client.baseURL ?? undefined, + }; + // Merge any upstream client envs from the request header, with upstream values taking precedence. + const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs }; + + // Setting a Stainless API key authenticates requests to the code tool endpoint. + const res = await fetch(codeModeEndpoint, { + method: 'POST', + headers: { + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), + 'Content-Type': 'application/json', + 'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs), + }, + body: JSON.stringify({ + project_name: 'ark', + code, + intent, + client_opts: {}, + } satisfies WorkerInput), + }); + + if (!res.ok) { + if (res.status === 404 && !reqContext.stainlessApiKey) { throw new Error( - `${res.status}: ${ - res.statusText - } error when trying to contact Code Tool server. Details: ${await res.text()}`, + 'Could not access code tool for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.', ); } + throw new Error( + `${res.status}: ${ + res.statusText + } error when trying to contact Code Tool server. Details: ${await res.text()}`, + ); + } - const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput; - const hasLogs = log_lines.length > 0 || err_lines.length > 0; - const output = { - result, - ...(log_lines.length > 0 && { log_lines }), - ...(err_lines.length > 0 && { err_lines }), - }; - if (is_error) { - return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2)); - } - return asTextContentResult(output); + const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput; + const hasLogs = log_lines.length > 0 || err_lines.length > 0; + const output = { + result, + ...(log_lines.length > 0 && { log_lines }), + ...(err_lines.length > 0 && { err_lines }), }; + if (is_error) { + return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2)); + } + return asTextContentResult(output); +}; - return { metadata, tool, handler }; -} +const localDenoHandler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: unknown; +}): Promise => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const url = await import('node:url'); + const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker'); + const { getWorkerPath } = await import('./code-tool-paths.cjs'); + const workerPath = getWorkerPath(); + + const client = reqContext.client; + const baseURLHostname = new URL(client.baseURL).hostname; + const { code } = args as { code: string }; + + let denoPath: string; + + const packageRoot = path.resolve(path.dirname(workerPath), '..'); + const packageNodeModulesPath = path.resolve(packageRoot, 'node_modules'); + + // Check if deno is in PATH + const { execSync } = await import('node:child_process'); + try { + execSync('command -v deno', { stdio: 'ignore' }); + denoPath = 'deno'; + } catch { + try { + // Use deno binary in node_modules if it's found + const denoNodeModulesPath = path.resolve(packageNodeModulesPath, 'deno', 'bin.cjs'); + await fs.promises.access(denoNodeModulesPath, fs.constants.X_OK); + denoPath = denoNodeModulesPath; + } catch { + return asErrorResult( + 'Deno is required for code execution but was not found. ' + + 'Install it from https://deno.land or run: npm install deno', + ); + } + } + + const allowReadPaths = [ + 'code-tool-worker.mjs', + `${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`, + packageRoot, + ]; + + // Follow symlinks in node_modules to allow read access to workspace-linked packages + try { + const sdkPkgName = 'ark-email'; + const sdkDir = path.resolve(packageNodeModulesPath, sdkPkgName); + const realSdkDir = fs.realpathSync(sdkDir); + if (realSdkDir !== sdkDir) { + allowReadPaths.push(realSdkDir); + } + } catch { + // Ignore if symlink resolution fails + } + + const allowRead = allowReadPaths.join(','); + + const worker = await newDenoHTTPWorker(url.pathToFileURL(workerPath), { + denoExecutable: denoPath, + runFlags: [ + `--node-modules-dir=manual`, + `--allow-read=${allowRead}`, + `--allow-net=${baseURLHostname}`, + // Allow environment variables because instantiating the client will try to read from them, + // even though they are not set. + '--allow-env', + ], + printOutput: true, + spawnOptions: { + cwd: path.dirname(workerPath), + // Merge any upstream client envs into the Deno subprocess environment, + // with the upstream env vars taking precedence. + env: { ...process.env, ...reqContext.upstreamClientEnvs }, + }, + }); + + try { + const resp = await new Promise((resolve, reject) => { + worker.addEventListener('exit', (exitCode) => { + reject(new Error(`Worker exited with code ${exitCode}`)); + }); + + // Strip null/undefined values so that the worker SDK client can fall back to + // reading from environment variables (including any upstreamClientEnvs). + const opts = { + ...(client.baseURL != null ? { baseURL: client.baseURL } : undefined), + ...(client.apiKey != null ? { apiKey: client.apiKey } : undefined), + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + } satisfies Partial as ClientOptions; + + const req = worker.request( + 'http://localhost', + { + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }, + (resp) => { + const body: Uint8Array[] = []; + resp.on('error', (err) => { + reject(err); + }); + resp.on('data', (chunk) => { + body.push(chunk); + }); + resp.on('end', () => { + resolve( + new Response(Buffer.concat(body).toString(), { + status: resp.statusCode ?? 200, + headers: resp.headers as any, + }), + ); + }); + }, + ); + + const body = JSON.stringify({ + opts, + code, + }); + + req.write(body, (err) => { + if (err != null) { + reject(err); + } + }); + + req.end(); + }); + + if (resp.status === 200) { + const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput; + const returnOutput: ContentBlock | null = + result == null ? null : ( + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); + const logOutput: ContentBlock | null = + log_lines.length === 0 ? + null + : { + type: 'text', + text: log_lines.join('\n'), + }; + const errOutput: ContentBlock | null = + err_lines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + err_lines.join('\n'), + }; + return { + content: [returnOutput, logOutput, errOutput].filter((block) => block !== null), + }; + } else { + const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput; + const messageOutput: ContentBlock | null = + result == null ? null : ( + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); + const logOutput: ContentBlock | null = + log_lines.length === 0 ? + null + : { + type: 'text', + text: log_lines.join('\n'), + }; + const errOutput: ContentBlock | null = + err_lines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + err_lines.join('\n'), + }; + return { + content: [messageOutput, logOutput, errOutput].filter((block) => block !== null), + isError: true, + }; + } + } finally { + worker.terminate(); + } +}; diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 90027a1..3828b0b 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,7 +1,9 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, McpRequestContext, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { Metadata, McpRequestContext, asTextContentResult } from './types'; +import { getLogger } from './logger'; +import type { LocalDocsSearch } from './local-docs-search'; export const metadata: Metadata = { resource: 'all', @@ -12,7 +14,8 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'search_docs', - description: 'Search for documentation for how to use the client to interact with the API.', + description: + 'Search SDK documentation to find methods, parameters, and usage examples for interacting with the API. Use this before writing code when you need to discover the right approach.', inputSchema: { type: 'object', properties: { @@ -41,28 +44,95 @@ export const tool: Tool = { const docsSearchURL = process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/ark/docs/search'; -export const handler = async ({ - reqContext, - args, -}: { - reqContext: McpRequestContext; - args: Record | undefined; -}) => { +let _localSearch: LocalDocsSearch | undefined; + +export function setLocalSearch(search: LocalDocsSearch): void { + _localSearch = search; +} + +async function searchLocal(args: Record): Promise { + if (!_localSearch) { + throw new Error('Local search not initialized'); + } + + const query = (args['query'] as string) ?? ''; + const language = (args['language'] as string) ?? 'typescript'; + const detail = (args['detail'] as string) ?? 'default'; + + return _localSearch.search({ + query, + language, + detail, + maxResults: 10, + }).results; +} + +async function searchRemote(args: Record, reqContext: McpRequestContext): Promise { const body = args as any; const query = new URLSearchParams(body).toString(); + + const startTime = Date.now(); const result = await fetch(`${docsSearchURL}?${query}`, { headers: { ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), + ...(reqContext.mcpSessionId && { 'x-stainless-mcp-session-id': reqContext.mcpSessionId }), + ...(reqContext.mcpClientInfo && { + 'x-stainless-mcp-client-info': JSON.stringify(reqContext.mcpClientInfo), + }), }, }); + const logger = getLogger(); + if (!result.ok) { + const errorText = await result.text(); + logger.warn( + { + durationMs: Date.now() - startTime, + query: body.query, + status: result.status, + statusText: result.statusText, + errorText, + }, + 'Got error response from docs search tool', + ); + + if (result.status === 404 && !reqContext.stainlessApiKey) { + throw new Error( + 'Could not find docs for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.', + ); + } + throw new Error( - `${result.status}: ${result.statusText} when using doc search tool. Details: ${await result.text()}`, + `${result.status}: ${result.statusText} when using doc search tool. Details: ${errorText}`, ); } - return asTextContentResult(await result.json()); + const resultBody = await result.json(); + logger.info( + { + durationMs: Date.now() - startTime, + query: body.query, + }, + 'Got docs search result', + ); + return resultBody; +} + +export const handler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => { + const body = args ?? {}; + + if (_localSearch) { + return asTextContentResult(await searchLocal(body)); + } + + return asTextContentResult(await searchRemote(body, reqContext)); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 9cce605..29110c1 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -4,9 +4,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { ClientOptions } from 'ark-email'; import express from 'express'; -import morgan from 'morgan'; -import morganBody from 'morgan-body'; +import pino from 'pino'; +import pinoHttp from 'pino-http'; import { getStainlessApiKey, parseClientAuthHeaders } from './auth'; +import { getLogger } from './logger'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; @@ -22,29 +23,72 @@ const newServer = async ({ res: express.Response; }): Promise => { const stainlessApiKey = getStainlessApiKey(req, mcpOptions); - const server = await newMcpServer(stainlessApiKey); + const customInstructionsPath = mcpOptions.customInstructionsPath; + const server = await newMcpServer({ stainlessApiKey, customInstructionsPath }); - try { - const authOptions = parseClientAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); - await initMcpServer({ - server: server, - mcpOptions: mcpOptions, - clientOptions: { - ...clientOptions, - ...authOptions, - }, - stainlessApiKey: stainlessApiKey, - }); - } catch (error) { - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Unauthorized: ${error instanceof Error ? error.message : error}`, - }, - }); - return null; + let upstreamClientEnvs: Record | undefined; + const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs']; + if (typeof clientEnvsHeader === 'string') { + try { + const parsed = JSON.parse(clientEnvsHeader); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + upstreamClientEnvs = parsed; + } + } catch { + // Ignore malformed header + } + } + + // Parse x-stainless-mcp-client-permissions header to override permission options + // + // Note: Permissions are best-effort and intended to prevent clients from doing unexpected things; + // they're not a hard security boundary, so we allow arbitrary, client-driven overrides. + // + // See the Stainless MCP documentation for more details. + let effectiveMcpOptions = mcpOptions; + const clientPermissionsHeader = req.headers['x-stainless-mcp-client-permissions']; + if (typeof clientPermissionsHeader === 'string') { + try { + const parsed = JSON.parse(clientPermissionsHeader); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + effectiveMcpOptions = { + ...mcpOptions, + ...(typeof parsed.allow_http_gets === 'boolean' && { codeAllowHttpGets: parsed.allow_http_gets }), + ...(Array.isArray(parsed.allowed_methods) && { codeAllowedMethods: parsed.allowed_methods }), + ...(Array.isArray(parsed.blocked_methods) && { codeBlockedMethods: parsed.blocked_methods }), + }; + getLogger().info( + { clientPermissions: parsed }, + 'Overriding code execution permissions from x-stainless-mcp-client-permissions header', + ); + } + } catch (error) { + getLogger().warn({ error }, 'Failed to parse x-stainless-mcp-client-permissions header'); + } + } + + const mcpClientInfo = + typeof req.body?.params?.clientInfo?.name === 'string' ? + { name: req.body.params.clientInfo.name, version: String(req.body.params.clientInfo.version ?? '') } + : undefined; + + await initMcpServer({ + server: server, + mcpOptions: effectiveMcpOptions, + clientOptions: { + ...clientOptions, + ...authOptions, + }, + stainlessApiKey: stainlessApiKey, + upstreamClientEnvs, + mcpSessionId: (req as any).mcpSessionId, + mcpClientInfo, + }); + + if (mcpClientInfo) { + getLogger().info({ mcpSessionId: (req as any).mcpSessionId, mcpClientInfo }, 'MCP client connected'); } return server; @@ -81,29 +125,74 @@ const del = async (req: express.Request, res: express.Response) => { }); }; +const redactHeaders = (headers: Record) => { + const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i; + const filtered = { ...headers }; + Object.keys(filtered).forEach((key) => { + if (hiddenHeaders.test(key)) { + filtered[key] = '[REDACTED]'; + } + }); + return filtered; +}; + export const streamableHTTPApp = ({ clientOptions = {}, mcpOptions, - debug, }: { clientOptions?: ClientOptions; mcpOptions: McpOptions; - debug: boolean; }): express.Express => { const app = express(); app.set('query parser', 'extended'); app.use(express.json()); - - if (debug) { - morganBody(app, { - logAllReqHeader: true, - logAllResHeader: true, - logRequestBody: true, - logResponseBody: true, - }); - } else { - app.use(morgan('combined')); - } + app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { + const existing = req.headers['mcp-session-id']; + const sessionId = (Array.isArray(existing) ? existing[0] : existing) || crypto.randomUUID(); + (req as any).mcpSessionId = sessionId; + const origWriteHead = res.writeHead.bind(res); + res.writeHead = function (statusCode: number, ...rest: any[]) { + res.setHeader('mcp-session-id', sessionId); + return origWriteHead(statusCode, ...rest); + } as typeof res.writeHead; + next(); + }); + app.use( + pinoHttp({ + logger: getLogger(), + customProps: (req) => ({ + mcpSessionId: (req as any).mcpSessionId, + }), + customLogLevel: (req, res) => { + if (res.statusCode >= 500) { + return 'error'; + } else if (res.statusCode >= 400) { + return 'warn'; + } + return 'info'; + }, + customSuccessMessage: function (req, res) { + return `Request ${req.method} to ${req.url} completed with status ${res.statusCode}`; + }, + customErrorMessage: function (req, res, err) { + return `Request ${req.method} to ${req.url} errored with status ${res.statusCode}`; + }, + serializers: { + req: pino.stdSerializers.wrapRequestSerializer((req) => { + return { + ...req, + headers: redactHeaders(req.raw.headers), + }; + }), + res: pino.stdSerializers.wrapResponseSerializer((res) => { + return { + ...res, + headers: redactHeaders(res.headers), + }; + }), + }, + }), + ); app.get('/health', async (req: express.Request, res: express.Response) => { res.status(200).send('OK'); @@ -117,22 +206,22 @@ export const streamableHTTPApp = ({ export const launchStreamableHTTPServer = async ({ mcpOptions, - debug, port, }: { mcpOptions: McpOptions; - debug: boolean; port: number | string | undefined; }) => { - const app = streamableHTTPApp({ mcpOptions, debug }); + const app = streamableHTTPApp({ mcpOptions }); const server = app.listen(port); const address = server.address(); + const logger = getLogger(); + if (typeof address === 'string') { - console.error(`MCP Server running on streamable HTTP at ${address}`); + logger.info(`MCP Server running on streamable HTTP at ${address}`); } else if (address !== null) { - console.error(`MCP Server running on streamable HTTP on port ${address.port}`); + logger.info(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${port}`); + logger.info(`MCP Server running on streamable HTTP on port ${port}`); } }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 003a765..5bca4a6 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -5,15 +5,20 @@ import { McpOptions, parseCLIOptions } from './options'; import { launchStdioServer } from './stdio'; import { launchStreamableHTTPServer } from './http'; import type { McpTool } from './types'; +import { configureLogger, getLogger } from './logger'; async function main() { const options = parseOptionsOrError(); + configureLogger({ + level: options.debug ? 'debug' : 'info', + pretty: options.logFormat === 'pretty', + }); const selectedTools = await selectToolsOrError(options); - console.error( - `MCP Server starting with ${selectedTools.length} tools:`, - selectedTools.map((e) => e.tool.name), + getLogger().info( + { tools: selectedTools.map((e) => e.tool.name) }, + `MCP Server starting with ${selectedTools.length} tools`, ); switch (options.transport) { @@ -23,8 +28,7 @@ async function main() { case 'http': await launchStreamableHTTPServer({ mcpOptions: options, - debug: options.debug, - port: options.port ?? options.socket, + port: options.socket ?? options.port, }); break; } @@ -32,7 +36,8 @@ async function main() { if (require.main === module) { main().catch((error) => { - console.error('Fatal error in main():', error); + // Logger might not be initialized yet + console.error('Fatal error in main()', error); process.exit(1); }); } @@ -41,7 +46,8 @@ function parseOptionsOrError() { try { return parseCLIOptions(); } catch (error) { - console.error('Error parsing options:', error); + // Logger is initialized after options, so use console.error here + console.error('Error parsing options', error); process.exit(1); } } @@ -50,16 +56,12 @@ async function selectToolsOrError(options: McpOptions): Promise { try { const includedTools = selectTools(options); if (includedTools.length === 0) { - console.error('No tools match the provided filters.'); + getLogger().error('No tools match the provided filters'); process.exit(1); } return includedTools; } catch (error) { - if (error instanceof Error) { - console.error('Error filtering tools:', error.message); - } else { - console.error('Error filtering tools:', error); - } + getLogger().error({ error }, 'Error filtering tools'); process.exit(1); } } diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts new file mode 100644 index 0000000..5208ca3 --- /dev/null +++ b/packages/mcp-server/src/instructions.ts @@ -0,0 +1,83 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import fs from 'fs/promises'; +import { getLogger } from './logger'; +import { readEnv } from './util'; + +const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes + +interface InstructionsCacheEntry { + fetchedInstructions: string; + fetchedAt: number; +} + +const instructionsCache = new Map(); + +export async function getInstructions({ + stainlessApiKey, + customInstructionsPath, +}: { + stainlessApiKey?: string | undefined; + customInstructionsPath?: string | undefined; +}): Promise { + const now = Date.now(); + const cacheKey = customInstructionsPath ?? stainlessApiKey ?? ''; + const cached = instructionsCache.get(cacheKey); + + if (cached && now - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) { + return cached.fetchedInstructions; + } + + // Evict stale entries so the cache doesn't grow unboundedly. + for (const [key, entry] of instructionsCache) { + if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) { + instructionsCache.delete(key); + } + } + + let fetchedInstructions: string; + + if (customInstructionsPath) { + fetchedInstructions = await fetchLatestInstructionsFromFile(customInstructionsPath); + } else { + fetchedInstructions = await fetchLatestInstructionsFromApi(stainlessApiKey); + } + + instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: now }); + return fetchedInstructions; +} + +async function fetchLatestInstructionsFromFile(path: string): Promise { + try { + return await fs.readFile(path, 'utf-8'); + } catch (error) { + getLogger().error({ error, path }, 'Error fetching instructions from file'); + throw error; + } +} + +async function fetchLatestInstructionsFromApi(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. + const response = await fetch( + readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/ark', + { + method: 'GET', + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, + }, + ); + + let instructions: string | undefined; + if (!response.ok) { + getLogger().warn( + 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', + ); + + instructions = + '\n This is the ark MCP server.\n\n Available tools:\n - search_docs: Search SDK documentation to find the right methods and parameters.\n - execute: Run TypeScript code against a pre-authenticated SDK client. Define an async run(client) function.\n\n Workflow:\n - If unsure about the API, call search_docs first.\n - Write complete solutions in a single execute call when possible. For large datasets, use API filters to narrow results or paginate within a single execute block.\n - If execute returns an error, read the error and fix your code rather than retrying the same approach.\n - Variables do not persist between execute calls. Return or log all data you need.\n - Individual HTTP requests to the API have a 30-second timeout. If a request times out, try a smaller query or add filters.\n - Code execution has a total timeout of approximately 5 minutes. If your code times out, simplify it or break it into smaller steps.\n '; + } + + instructions ??= ((await response.json()) as { instructions: string }).instructions; + + return instructions; +} diff --git a/packages/mcp-server/src/local-docs-search.ts b/packages/mcp-server/src/local-docs-search.ts new file mode 100644 index 0000000..c332be0 --- /dev/null +++ b/packages/mcp-server/src/local-docs-search.ts @@ -0,0 +1,2819 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import MiniSearch from 'minisearch'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { getLogger } from './logger'; + +type PerLanguageData = { + method?: string; + example?: string; +}; + +type MethodEntry = { + name: string; + endpoint: string; + httpMethod: string; + summary: string; + description: string; + stainlessPath: string; + qualified: string; + params?: string[]; + response?: string; + markdown?: string; + perLanguage?: Record; +}; + +type ProseChunk = { + content: string; + tag: string; + sectionContext?: string; + source?: string; +}; + +type MiniSearchDocument = { + id: string; + kind: 'http_method' | 'prose'; + name?: string; + endpoint?: string; + summary?: string; + description?: string; + qualified?: string; + stainlessPath?: string; + content?: string; + sectionContext?: string; + _original: Record; +}; + +type SearchResult = { + results: (string | Record)[]; +}; + +const EMBEDDED_METHODS: MethodEntry[] = [ + { + name: 'send', + endpoint: '/emails', + httpMethod: 'post', + summary: 'Send an email', + description: + 'Send a single email message. The email is accepted for immediate delivery\nand typically delivered within seconds.\n\n**Example use case:** Send a password reset email to a user.\n\n**Required fields:** `from`, `to`, `subject`, and either `html` or `text`\n\n**Idempotency:** Supports `Idempotency-Key` header for safe retries.\n\n**Related endpoints:**\n- `GET /emails/{emailId}` - Track delivery status\n- `GET /emails/{emailId}/deliveries` - View delivery attempts\n- `POST /emails/{emailId}/retry` - Retry failed delivery\n', + stainlessPath: '(resource) emails > (method) send', + qualified: 'client.emails.send', + params: [ + 'from: string;', + 'subject: string;', + 'to: string[];', + 'attachments?: { content: string; contentType: string; filename: string; }[];', + 'bcc?: string[];', + 'cc?: string[];', + 'headers?: object;', + 'html?: string;', + 'metadata?: object;', + 'replyTo?: string;', + 'tag?: string;', + 'tenantId?: string;', + 'text?: string;', + 'Idempotency-Key?: string;', + ], + response: + "{ data: { id: string; status: 'pending' | 'sent'; tenantId: string; to: string[]; messageId?: string; sandbox?: boolean; }; meta: { requestId: string; }; success: true; }", + markdown: + "## send\n\n`client.emails.send(from: string, subject: string, to: string[], attachments?: { content: string; contentType: string; filename: string; }[], bcc?: string[], cc?: string[], headers?: object, html?: string, metadata?: object, replyTo?: string, tag?: string, tenantId?: string, text?: string, Idempotency-Key?: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/emails`\n\nSend a single email message. The email is accepted for immediate delivery\nand typically delivered within seconds.\n\n**Example use case:** Send a password reset email to a user.\n\n**Required fields:** `from`, `to`, `subject`, and either `html` or `text`\n\n**Idempotency:** Supports `Idempotency-Key` header for safe retries.\n\n**Related endpoints:**\n- `GET /emails/{emailId}` - Track delivery status\n- `GET /emails/{emailId}/deliveries` - View delivery attempts\n- `POST /emails/{emailId}/retry` - Retry failed delivery\n\n\n### Parameters\n\n- `from: string`\n Sender email address. Must be from a verified domain OR use sandbox mode.\n\n**Supported formats:**\n- Email only: `hello@yourdomain.com`\n- With display name: `Acme `\n- With quoted name: `\"Acme Support\" `\n\nThe domain portion must match a verified sending domain in your account.\n\n**Sandbox mode:** Use `sandbox@arkhq.io` to send test emails without domain verification.\nSandbox emails can only be sent to organization members and are limited to 10 per day.\n\n\n- `subject: string`\n Email subject line\n\n- `to: string[]`\n Recipient email addresses (max 50)\n\n- `attachments?: { content: string; contentType: string; filename: string; }[]`\n File attachments (accepts null)\n\n- `bcc?: string[]`\n BCC recipients (accepts null)\n\n- `cc?: string[]`\n CC recipients (accepts null)\n\n- `headers?: object`\n Custom email headers (accepts null)\n\n- `html?: string`\n HTML body content (accepts null).\nMaximum 5MB (5,242,880 characters). Combined with attachments,\nthe total message must not exceed 14MB.\n\n\n- `metadata?: object`\n Custom key-value pairs attached to an email for webhook correlation.\n\nWhen you send an email with metadata, these key-value pairs are:\n- **Stored** with the message\n- **Returned** in all webhook event payloads (MessageSent, MessageBounced, etc.)\n- **Never visible** to email recipients\n\nThis is useful for correlating webhook events with your internal systems\n(e.g., user IDs, order IDs, campaign identifiers).\n\n**Validation Rules:**\n- Maximum 10 keys per email\n- Keys: 1-40 characters, must start with a letter, only alphanumeric and underscores (`^[a-zA-Z][a-zA-Z0-9_]*$`)\n- Values: 1-500 characters, no control characters (newlines, tabs, etc.)\n- Total size: 4KB maximum (JSON-encoded)\n\n\n- `replyTo?: string`\n Reply-to address (accepts null)\n\n- `tag?: string`\n Tag for categorization and filtering (accepts null)\n\n- `tenantId?: string`\n The tenant ID to send this email from. Determines which tenant's\nconfiguration (domains, webhooks, tracking) is used.\n\n- If your API key is scoped to a specific tenant, this must match that tenant or be omitted.\n- If your API key is org-level, specify the tenant to send from.\n- If omitted, the organization's default tenant is used.\n\n\n- `text?: string`\n Plain text body (accepts null, auto-generated from HTML if not provided).\nMaximum 5MB (5,242,880 characters).\n\n\n- `Idempotency-Key?: string`\n\n### Returns\n\n- `{ data: { id: string; status: 'pending' | 'sent'; tenantId: string; to: string[]; messageId?: string; sandbox?: boolean; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; status: 'pending' | 'sent'; tenantId: string; to: string[]; messageId?: string; sandbox?: boolean; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.emails.send({\n from: 'Acme ',\n subject: 'Hello World',\n to: ['user@example.com'],\n});\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Emails.Send', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Emails.Send(context.TODO(), ark.EmailSendParams{\n\t\tFrom: "Acme ",\n\t\tSubject: "Hello World",\n\t\tTo: []string{"user@example.com"},\n\t\tHTML: ark.String("

Welcome!

Thanks for signing up.

"),\n\t\tMetadata: map[string]string{\n\t\t\t"user_id": "usr_123",\n\t\t\t"campaign": "onboarding",\n\t\t},\n\t\tTenantID: ark.String("cm6abc123def456"),\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/emails \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "from": "Acme ",\n "subject": "Hello World",\n "to": [\n "user@example.com"\n ],\n "metadata": {\n "user_id": "usr_123",\n "campaign": "onboarding"\n },\n "tenantId": "cm6abc123def456"\n }\'', + }, + python: { + method: 'emails.send', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.emails.send(\n from_="Acme ",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

Thanks for signing up.

",\n metadata={\n "user_id": "usr_123",\n "campaign": "onboarding",\n },\n tenant_id="cm6abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'emails.send_', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.emails.send_(from: "Acme ", subject: "Hello World", to: ["user@example.com"])\n\nputs(response)', + }, + typescript: { + method: 'client.emails.send', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.emails.send({\n from: 'Acme ',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

Thanks for signing up.

',\n metadata: { user_id: 'usr_123', campaign: 'onboarding' },\n tenantId: 'cm6abc123def456',\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/emails', + httpMethod: 'get', + summary: 'List sent emails', + description: + 'Retrieve a paginated list of sent emails. Results are ordered by\nsend time, newest first.\n\nUse filters to narrow down results by status, recipient, sender, or tag.\n\n**Related endpoints:**\n- `GET /emails/{emailId}` - Get full details of a specific email\n- `POST /emails` - Send a new email\n', + stainlessPath: '(resource) emails > (method) list', + qualified: 'client.emails.list', + params: [ + 'after?: string;', + 'before?: string;', + 'from?: string;', + 'page?: number;', + 'perPage?: number;', + "status?: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held';", + 'tag?: string;', + 'to?: string;', + ], + response: + "{ id: string; from: string; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'; subject: string; tenantId: string; timestamp: number; timestampIso: string; to: string; tag?: string; }", + markdown: + "## list\n\n`client.emails.list(after?: string, before?: string, from?: string, page?: number, perPage?: number, status?: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held', tag?: string, to?: string): { id: string; from: string; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'; subject: string; tenantId: string; timestamp: number; timestampIso: string; to: string; tag?: string; }`\n\n**get** `/emails`\n\nRetrieve a paginated list of sent emails. Results are ordered by\nsend time, newest first.\n\nUse filters to narrow down results by status, recipient, sender, or tag.\n\n**Related endpoints:**\n- `GET /emails/{emailId}` - Get full details of a specific email\n- `POST /emails` - Send a new email\n\n\n### Parameters\n\n- `after?: string`\n Return emails sent after this timestamp (Unix seconds or ISO 8601)\n\n- `before?: string`\n Return emails sent before this timestamp\n\n- `from?: string`\n Filter by sender email address\n\n- `page?: number`\n Page number (starts at 1)\n\n- `perPage?: number`\n Results per page (max 100)\n\n- `status?: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'`\n Filter by delivery status:\n- `pending` - Email accepted, waiting to be processed\n- `sent` - Email transmitted to recipient's mail server\n- `softfail` - Temporary delivery failure, will retry\n- `hardfail` - Permanent delivery failure\n- `bounced` - Email bounced back\n- `held` - Held for manual review\n\n- `tag?: string`\n Filter by tag\n\n- `to?: string`\n Filter by recipient email address\n\n### Returns\n\n- `{ id: string; from: string; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'; subject: string; tenantId: string; timestamp: number; timestampIso: string; to: string; tag?: string; }`\n\n - `id: string`\n - `from: string`\n - `status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'`\n - `subject: string`\n - `tenantId: string`\n - `timestamp: number`\n - `timestampIso: string`\n - `to: string`\n - `tag?: string`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const emailListResponse of client.emails.list()) {\n console.log(emailListResponse);\n}\n```", + perLanguage: { + go: { + method: 'client.Emails.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Emails.List(context.TODO(), ark.EmailListParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/emails \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'emails.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.emails.list()\npage = page.data[0]\nprint(page.id)', + }, + ruby: { + method: 'emails.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.emails.list\n\nputs(page)', + }, + typescript: { + method: 'client.emails.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const emailListResponse of client.emails.list()) {\n console.log(emailListResponse.id);\n}", + }, + }, + }, + { + name: 'send_batch', + endpoint: '/emails/batch', + httpMethod: 'post', + summary: 'Send multiple emails', + description: + "Send up to 100 emails in a single request. Useful for sending\npersonalized emails to multiple recipients efficiently.\n\nEach email in the batch can have different content and recipients.\nFailed emails don't affect other emails in the batch.\n\n**Idempotency:** Supports `Idempotency-Key` header for safe retries.\n", + stainlessPath: '(resource) emails > (method) send_batch', + qualified: 'client.emails.sendBatch', + params: [ + 'emails: { subject: string; to: string[]; html?: string; metadata?: object; tag?: string; text?: string; }[];', + 'from: string;', + 'tenantId?: string;', + 'Idempotency-Key?: string;', + ], + response: + '{ data: { accepted: number; failed: number; messages: object; tenantId: string; total: number; sandbox?: boolean; }; meta: { requestId: string; }; success: true; }', + markdown: + "## send_batch\n\n`client.emails.sendBatch(emails: { subject: string; to: string[]; html?: string; metadata?: object; tag?: string; text?: string; }[], from: string, tenantId?: string, Idempotency-Key?: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/emails/batch`\n\nSend up to 100 emails in a single request. Useful for sending\npersonalized emails to multiple recipients efficiently.\n\nEach email in the batch can have different content and recipients.\nFailed emails don't affect other emails in the batch.\n\n**Idempotency:** Supports `Idempotency-Key` header for safe retries.\n\n\n### Parameters\n\n- `emails: { subject: string; to: string[]; html?: string; metadata?: object; tag?: string; text?: string; }[]`\n\n- `from: string`\n Sender email for all messages\n\n- `tenantId?: string`\n The tenant ID to send this batch from. Determines which tenant's\nconfiguration (domains, webhooks, tracking) is used.\n\n- If your API key is scoped to a specific tenant, this must match that tenant or be omitted.\n- If your API key is org-level, specify the tenant to send from.\n- If omitted, the organization's default tenant is used.\n\n\n- `Idempotency-Key?: string`\n\n### Returns\n\n- `{ data: { accepted: number; failed: number; messages: object; tenantId: string; total: number; sandbox?: boolean; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { accepted: number; failed: number; messages: object; tenantId: string; total: number; sandbox?: boolean; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.emails.sendBatch({ emails: [{ subject: 'Hello Alice', to: ['alice@example.com'] }, { subject: 'Hello Bob', to: ['bob@example.com'] }], from: 'notifications@myapp.com' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Emails.SendBatch', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Emails.SendBatch(context.TODO(), ark.EmailSendBatchParams{\n\t\tEmails: []ark.EmailSendBatchParamsEmail{{\n\t\t\tTo: []string{"alice@example.com"},\n\t\t\tSubject: "Hello Alice",\n\t\t\tHTML: ark.String("

Hi Alice, your order is ready!

"),\n\t\t\tTag: ark.String("order-ready"),\n\t\t}, {\n\t\t\tTo: []string{"bob@example.com"},\n\t\t\tSubject: "Hello Bob",\n\t\t\tHTML: ark.String("

Hi Bob, your order is ready!

"),\n\t\t\tTag: ark.String("order-ready"),\n\t\t}},\n\t\tFrom: "notifications@myapp.com",\n\t\tTenantID: ark.String("cm6abc123def456"),\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/emails/batch \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "emails": [\n {\n "subject": "Hello Alice",\n "to": [\n "alice@example.com"\n ],\n "html": "

Hi Alice, your order is ready!

",\n "tag": "order-ready"\n },\n {\n "subject": "Hello Bob",\n "to": [\n "bob@example.com"\n ],\n "html": "

Hi Bob, your order is ready!

",\n "tag": "order-ready"\n }\n ],\n "from": "notifications@myapp.com",\n "tenantId": "cm6abc123def456"\n }\'', + }, + python: { + method: 'emails.send_batch', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.emails.send_batch(\n emails=[{\n "to": ["alice@example.com"],\n "subject": "Hello Alice",\n "html": "

Hi Alice, your order is ready!

",\n "tag": "order-ready",\n }, {\n "to": ["bob@example.com"],\n "subject": "Hello Bob",\n "html": "

Hi Bob, your order is ready!

",\n "tag": "order-ready",\n }],\n from_="notifications@myapp.com",\n tenant_id="cm6abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'emails.send_batch', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.emails.send_batch(\n emails: [{subject: "Hello Alice", to: ["alice@example.com"]}, {subject: "Hello Bob", to: ["bob@example.com"]}],\n from: "notifications@myapp.com"\n)\n\nputs(response)', + }, + typescript: { + method: 'client.emails.sendBatch', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.emails.sendBatch({\n emails: [\n {\n to: ['alice@example.com'],\n subject: 'Hello Alice',\n html: '

Hi Alice, your order is ready!

',\n tag: 'order-ready',\n },\n {\n to: ['bob@example.com'],\n subject: 'Hello Bob',\n html: '

Hi Bob, your order is ready!

',\n tag: 'order-ready',\n },\n ],\n from: 'notifications@myapp.com',\n tenantId: 'cm6abc123def456',\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'send_raw', + endpoint: '/emails/raw', + httpMethod: 'post', + summary: 'Send raw MIME email', + description: + 'Send a pre-formatted RFC 2822 MIME message. Use this for advanced\nuse cases or when migrating from systems that generate raw email content.\n\n**Important:** The `rawMessage` field must be base64-encoded. Your raw MIME\nmessage (with headers like From, To, Subject, Content-Type, followed by a\nblank line and the body) must be encoded to base64 before sending.\n', + stainlessPath: '(resource) emails > (method) send_raw', + qualified: 'client.emails.sendRaw', + params: [ + 'from: string;', + 'rawMessage: string;', + 'to: string[];', + 'bounce?: boolean;', + 'tenantId?: string;', + ], + response: + "{ data: { id: string; status: 'pending' | 'sent'; tenantId: string; to: string[]; messageId?: string; sandbox?: boolean; }; meta: { requestId: string; }; success: true; }", + markdown: + "## send_raw\n\n`client.emails.sendRaw(from: string, rawMessage: string, to: string[], bounce?: boolean, tenantId?: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/emails/raw`\n\nSend a pre-formatted RFC 2822 MIME message. Use this for advanced\nuse cases or when migrating from systems that generate raw email content.\n\n**Important:** The `rawMessage` field must be base64-encoded. Your raw MIME\nmessage (with headers like From, To, Subject, Content-Type, followed by a\nblank line and the body) must be encoded to base64 before sending.\n\n\n### Parameters\n\n- `from: string`\n Sender email address. Must be from a verified domain.\n\n**Supported formats:**\n- Email only: `hello@yourdomain.com`\n- With display name: `Acme `\n- With quoted name: `\"Acme Support\" `\n\nThe domain portion must match a verified sending domain in your account.\n\n\n- `rawMessage: string`\n Base64-encoded RFC 2822 MIME message.\n\n**You must base64-encode your raw email before sending.** The raw email\nshould include headers (From, To, Subject, Content-Type, etc.) followed\nby a blank line and the message body.\n\n\n- `to: string[]`\n Recipient email addresses\n\n- `bounce?: boolean`\n Whether this is a bounce message (accepts null)\n\n- `tenantId?: string`\n The tenant ID to send this email from. Determines which tenant's\nconfiguration (domains, webhooks, tracking) is used.\n\n- If your API key is scoped to a specific tenant, this must match that tenant or be omitted.\n- If your API key is org-level, specify the tenant to send from.\n- If omitted, the organization's default tenant is used.\n\n\n### Returns\n\n- `{ data: { id: string; status: 'pending' | 'sent'; tenantId: string; to: string[]; messageId?: string; sandbox?: boolean; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; status: 'pending' | 'sent'; tenantId: string; to: string[]; messageId?: string; sandbox?: boolean; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.emails.sendRaw({\n from: 'Acme ',\n rawMessage: 'x',\n to: ['user@example.com'],\n});\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Emails.SendRaw', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Emails.SendRaw(context.TODO(), ark.EmailSendRawParams{\n\t\tFrom: "Acme ",\n\t\tRawMessage: "x",\n\t\tTo: []string{"user@example.com"},\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/emails/raw \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "from": "Acme ",\n "rawMessage": "x",\n "to": [\n "user@example.com"\n ],\n "tenantId": "cm6abc123def456"\n }\'', + }, + python: { + method: 'emails.send_raw', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.emails.send_raw(\n from_="Acme ",\n raw_message="x",\n to=["user@example.com"],\n)\nprint(response.data)', + }, + ruby: { + method: 'emails.send_raw', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.emails.send_raw(from: "Acme ", raw_message: "x", to: ["user@example.com"])\n\nputs(response)', + }, + typescript: { + method: 'client.emails.sendRaw', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.emails.sendRaw({\n from: 'Acme ',\n rawMessage: 'x',\n to: ['user@example.com'],\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/emails/{emailId}', + httpMethod: 'get', + summary: 'Get email details', + description: + 'Retrieve detailed information about a specific email including\ndelivery status, timestamps, and optionally the email content.\n\nUse the `expand` parameter to include additional data like the\nHTML/text body, headers, or delivery attempts.\n', + stainlessPath: '(resource) emails > (method) retrieve', + qualified: 'client.emails.retrieve', + params: ['emailId: string;', 'expand?: string;'], + response: + "{ data: { id: string; from: string; scope: 'outgoing' | 'incoming'; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'; subject: string; tenantId: string; timestamp: number; timestampIso: string; to: string; activity?: { clicks?: object[]; opens?: object[]; }; attachments?: { contentType: string; data: string; filename: string; hash: string; size: number; }[]; deliveries?: { id: string; status: string; timestamp: number; timestampIso: string; classification?: string; classificationCode?: number; code?: number; details?: string; output?: string; remoteHost?: string; sentWithSsl?: boolean; smtpEnhancedCode?: string; }[]; headers?: object; htmlBody?: string; messageId?: string; plainBody?: string; rawMessage?: string; spam?: boolean; spamScore?: number; tag?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## retrieve\n\n`client.emails.retrieve(emailId: string, expand?: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/emails/{emailId}`\n\nRetrieve detailed information about a specific email including\ndelivery status, timestamps, and optionally the email content.\n\nUse the `expand` parameter to include additional data like the\nHTML/text body, headers, or delivery attempts.\n\n\n### Parameters\n\n- `emailId: string`\n\n- `expand?: string`\n Comma-separated list of fields to include:\n- `full` - Include all expanded fields in a single request\n- `content` - HTML and plain text body\n- `headers` - Email headers\n- `deliveries` - Delivery attempt history\n- `activity` - Opens and clicks tracking data\n- `attachments` - File attachments with content (base64 encoded)\n- `raw` - Complete raw MIME message (base64 encoded)\n\n\n### Returns\n\n- `{ data: { id: string; from: string; scope: 'outgoing' | 'incoming'; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'; subject: string; tenantId: string; timestamp: number; timestampIso: string; to: string; activity?: { clicks?: object[]; opens?: object[]; }; attachments?: { contentType: string; data: string; filename: string; hash: string; size: number; }[]; deliveries?: { id: string; status: string; timestamp: number; timestampIso: string; classification?: string; classificationCode?: number; code?: number; details?: string; output?: string; remoteHost?: string; sentWithSsl?: boolean; smtpEnhancedCode?: string; }[]; headers?: object; htmlBody?: string; messageId?: string; plainBody?: string; rawMessage?: string; spam?: boolean; spamScore?: number; tag?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; from: string; scope: 'outgoing' | 'incoming'; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'bounced' | 'held'; subject: string; tenantId: string; timestamp: number; timestampIso: string; to: string; activity?: { clicks?: { ipAddress?: string; timestamp?: number; timestampIso?: string; url?: string; userAgent?: string; }[]; opens?: { ipAddress?: string; timestamp?: number; timestampIso?: string; userAgent?: string; }[]; }; attachments?: { contentType: string; data: string; filename: string; hash: string; size: number; }[]; deliveries?: { id: string; status: string; timestamp: number; timestampIso: string; classification?: string; classificationCode?: number; code?: number; details?: string; output?: string; remoteHost?: string; sentWithSsl?: boolean; smtpEnhancedCode?: string; }[]; headers?: object; htmlBody?: string; messageId?: string; plainBody?: string; rawMessage?: string; spam?: boolean; spamScore?: number; tag?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst email = await client.emails.retrieve('aBc123XyZ');\n\nconsole.log(email);\n```", + perLanguage: { + go: { + method: 'client.Emails.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\temail, err := client.Emails.Get(\n\t\tcontext.TODO(),\n\t\t"aBc123XyZ",\n\t\tark.EmailGetParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", email.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/emails/$EMAIL_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'emails.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nemail = client.emails.retrieve(\n email_id="aBc123XyZ",\n)\nprint(email.data)', + }, + ruby: { + method: 'emails.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nemail = ark.emails.retrieve("aBc123XyZ")\n\nputs(email)', + }, + typescript: { + method: 'client.emails.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst email = await client.emails.retrieve('aBc123XyZ');\n\nconsole.log(email.data);", + }, + }, + }, + { + name: 'retry', + endpoint: '/emails/{emailId}/retry', + httpMethod: 'post', + summary: 'Retry email delivery', + description: + 'Retry delivery of a failed or soft-bounced email. Creates a new\ndelivery attempt.\n\nOnly works for emails that have failed or are in a retryable state.\n', + stainlessPath: '(resource) emails > (method) retry', + qualified: 'client.emails.retry', + params: ['emailId: string;'], + response: + '{ data: { id: string; message: string; tenantId: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retry\n\n`client.emails.retry(emailId: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/emails/{emailId}/retry`\n\nRetry delivery of a failed or soft-bounced email. Creates a new\ndelivery attempt.\n\nOnly works for emails that have failed or are in a retryable state.\n\n\n### Parameters\n\n- `emailId: string`\n\n### Returns\n\n- `{ data: { id: string; message: string; tenantId: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; message: string; tenantId: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.emails.retry('aBc123XyZ');\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Emails.Retry', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Emails.Retry(context.TODO(), "aBc123XyZ")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/emails/$EMAIL_ID/retry \\\n -X POST \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'emails.retry', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.emails.retry(\n "aBc123XyZ",\n)\nprint(response.data)', + }, + ruby: { + method: 'emails.retry_', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.emails.retry_("aBc123XyZ")\n\nputs(response)', + }, + typescript: { + method: 'client.emails.retry', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.emails.retry('aBc123XyZ');\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'retrieve_deliveries', + endpoint: '/emails/{emailId}/deliveries', + httpMethod: 'get', + summary: 'Get delivery attempts', + description: + 'Get the complete delivery history for an email, including SMTP response codes,\ntimestamps, and current retry state.\n\n## Response Fields\n\n### Status\nThe current status of the email:\n- `pending` - Awaiting first delivery attempt\n- `sent` - Successfully delivered to recipient server\n- `softfail` - Temporary failure, automatic retry scheduled\n- `hardfail` - Permanent failure, will not retry\n- `held` - Held for manual review\n- `bounced` - Bounced by recipient server\n\n### Retry State\nWhen the email is in the delivery queue (`pending` or `softfail` status),\n`retryState` provides information about the retry schedule:\n- `attempt` - Current attempt number (0 = first attempt)\n- `maxAttempts` - Maximum attempts before hard-fail (typically 18)\n- `attemptsRemaining` - Attempts left before hard-fail\n- `nextRetryAt` - When the next retry is scheduled (Unix timestamp)\n- `processing` - Whether the email is currently being processed\n- `manual` - Whether this was triggered by a manual retry\n\nWhen the email has finished processing (`sent`, `hardfail`, `held`, `bounced`),\n`retryState` is `null`.\n\n### Can Retry Manually\nIndicates whether you can call `POST /emails/{emailId}/retry` to manually retry\nthe email. This is `true` when the raw message content is still available\n(not expired due to retention policy).\n', + stainlessPath: '(resource) emails > (method) retrieve_deliveries', + qualified: 'client.emails.retrieveDeliveries', + params: ['emailId: string;'], + response: + "{ data: { id: string; canRetryManually: boolean; deliveries: { id: string; status: string; timestamp: number; timestampIso: string; classification?: string; classificationCode?: number; code?: number; details?: string; output?: string; remoteHost?: string; sentWithSsl?: boolean; smtpEnhancedCode?: string; }[]; retryState: { attempt: number; attemptsRemaining: number; manual: boolean; maxAttempts: number; processing: boolean; nextRetryAt?: number; nextRetryAtIso?: string; }; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'held' | 'bounced'; tenantId: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## retrieve_deliveries\n\n`client.emails.retrieveDeliveries(emailId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/emails/{emailId}/deliveries`\n\nGet the complete delivery history for an email, including SMTP response codes,\ntimestamps, and current retry state.\n\n## Response Fields\n\n### Status\nThe current status of the email:\n- `pending` - Awaiting first delivery attempt\n- `sent` - Successfully delivered to recipient server\n- `softfail` - Temporary failure, automatic retry scheduled\n- `hardfail` - Permanent failure, will not retry\n- `held` - Held for manual review\n- `bounced` - Bounced by recipient server\n\n### Retry State\nWhen the email is in the delivery queue (`pending` or `softfail` status),\n`retryState` provides information about the retry schedule:\n- `attempt` - Current attempt number (0 = first attempt)\n- `maxAttempts` - Maximum attempts before hard-fail (typically 18)\n- `attemptsRemaining` - Attempts left before hard-fail\n- `nextRetryAt` - When the next retry is scheduled (Unix timestamp)\n- `processing` - Whether the email is currently being processed\n- `manual` - Whether this was triggered by a manual retry\n\nWhen the email has finished processing (`sent`, `hardfail`, `held`, `bounced`),\n`retryState` is `null`.\n\n### Can Retry Manually\nIndicates whether you can call `POST /emails/{emailId}/retry` to manually retry\nthe email. This is `true` when the raw message content is still available\n(not expired due to retention policy).\n\n\n### Parameters\n\n- `emailId: string`\n\n### Returns\n\n- `{ data: { id: string; canRetryManually: boolean; deliveries: { id: string; status: string; timestamp: number; timestampIso: string; classification?: string; classificationCode?: number; code?: number; details?: string; output?: string; remoteHost?: string; sentWithSsl?: boolean; smtpEnhancedCode?: string; }[]; retryState: { attempt: number; attemptsRemaining: number; manual: boolean; maxAttempts: number; processing: boolean; nextRetryAt?: number; nextRetryAtIso?: string; }; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'held' | 'bounced'; tenantId: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; canRetryManually: boolean; deliveries: { id: string; status: string; timestamp: number; timestampIso: string; classification?: string; classificationCode?: number; code?: number; details?: string; output?: string; remoteHost?: string; sentWithSsl?: boolean; smtpEnhancedCode?: string; }[]; retryState: { attempt: number; attemptsRemaining: number; manual: boolean; maxAttempts: number; processing: boolean; nextRetryAt?: number; nextRetryAtIso?: string; }; status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'held' | 'bounced'; tenantId: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.emails.retrieveDeliveries('aBc123XyZ');\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Emails.GetDeliveries', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Emails.GetDeliveries(context.TODO(), "aBc123XyZ")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/emails/$EMAIL_ID/deliveries \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'emails.retrieve_deliveries', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.emails.retrieve_deliveries(\n "aBc123XyZ",\n)\nprint(response.data)', + }, + ruby: { + method: 'emails.retrieve_deliveries', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.emails.retrieve_deliveries("aBc123XyZ")\n\nputs(response)', + }, + typescript: { + method: 'client.emails.retrieveDeliveries', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.emails.retrieveDeliveries('aBc123XyZ');\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/logs', + httpMethod: 'get', + summary: 'List API request logs', + description: + 'Retrieve a paginated list of API request logs for debugging and monitoring.\nResults are ordered by timestamp, newest first.\n\n**Use cases:**\n- Debug integration issues by reviewing recent requests\n- Monitor error rates and response times\n- Audit API usage patterns\n\n**Filters:**\n- `status` - Filter by success or error category\n- `statusCode` - Filter by exact HTTP status code\n- `endpoint` - Filter by endpoint name (e.g., `emails.send`)\n- `credentialId` - Filter by API key\n- `startDate`/`endDate` - Filter by date range\n\n**Note:** Request and response bodies are only included when\nretrieving a single log entry with `GET /logs/{requestId}`.\n\n**Related endpoints:**\n- `GET /logs/{requestId}` - Get full log details with request/response bodies\n', + stainlessPath: '(resource) logs > (method) list', + qualified: 'client.logs.list', + params: [ + 'credentialId?: string;', + 'endDate?: string;', + 'endpoint?: string;', + 'page?: number;', + 'perPage?: number;', + 'requestId?: string;', + 'startDate?: string;', + "status?: 'success' | 'error';", + 'statusCode?: number;', + ], + response: + "{ context: { idempotencyKey?: string; ipAddress?: string; queryParams?: object; userAgent?: string; }; credential: { id: string; keyPrefix?: string; }; durationMs: number; endpoint: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; rateLimit: { limit?: number; limited?: boolean; remaining?: number; reset?: number; }; requestId: string; statusCode: number; timestamp: string; email?: { id?: string; recipientCount?: number; }; error?: { code?: string; message?: string; }; sdk?: { name?: string; version?: string; }; }", + markdown: + "## list\n\n`client.logs.list(credentialId?: string, endDate?: string, endpoint?: string, page?: number, perPage?: number, requestId?: string, startDate?: string, status?: 'success' | 'error', statusCode?: number): { context: object; credential: object; durationMs: number; endpoint: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; rateLimit: object; requestId: string; statusCode: number; timestamp: string; email?: object; error?: object; sdk?: object; }`\n\n**get** `/logs`\n\nRetrieve a paginated list of API request logs for debugging and monitoring.\nResults are ordered by timestamp, newest first.\n\n**Use cases:**\n- Debug integration issues by reviewing recent requests\n- Monitor error rates and response times\n- Audit API usage patterns\n\n**Filters:**\n- `status` - Filter by success or error category\n- `statusCode` - Filter by exact HTTP status code\n- `endpoint` - Filter by endpoint name (e.g., `emails.send`)\n- `credentialId` - Filter by API key\n- `startDate`/`endDate` - Filter by date range\n\n**Note:** Request and response bodies are only included when\nretrieving a single log entry with `GET /logs/{requestId}`.\n\n**Related endpoints:**\n- `GET /logs/{requestId}` - Get full log details with request/response bodies\n\n\n### Parameters\n\n- `credentialId?: string`\n Filter by API credential ID\n\n- `endDate?: string`\n Filter logs before this date (ISO 8601 format)\n\n- `endpoint?: string`\n Filter by endpoint name\n\n- `page?: number`\n Page number\n\n- `perPage?: number`\n Results per page (max 100)\n\n- `requestId?: string`\n Filter by request ID (partial match)\n\n- `startDate?: string`\n Filter logs after this date (ISO 8601 format)\n\n- `status?: 'success' | 'error'`\n Filter by status category:\n- `success` - Status codes < 400\n- `error` - Status codes >= 400\n\n- `statusCode?: number`\n Filter by exact HTTP status code (100-599)\n\n### Returns\n\n- `{ context: { idempotencyKey?: string; ipAddress?: string; queryParams?: object; userAgent?: string; }; credential: { id: string; keyPrefix?: string; }; durationMs: number; endpoint: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; rateLimit: { limit?: number; limited?: boolean; remaining?: number; reset?: number; }; requestId: string; statusCode: number; timestamp: string; email?: { id?: string; recipientCount?: number; }; error?: { code?: string; message?: string; }; sdk?: { name?: string; version?: string; }; }`\n API request log entry (list view)\n\n - `context: { idempotencyKey?: string; ipAddress?: string; queryParams?: object; userAgent?: string; }`\n - `credential: { id: string; keyPrefix?: string; }`\n - `durationMs: number`\n - `endpoint: string`\n - `method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'`\n - `path: string`\n - `rateLimit: { limit?: number; limited?: boolean; remaining?: number; reset?: number; }`\n - `requestId: string`\n - `statusCode: number`\n - `timestamp: string`\n - `email?: { id?: string; recipientCount?: number; }`\n - `error?: { code?: string; message?: string; }`\n - `sdk?: { name?: string; version?: string; }`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const logEntry of client.logs.list()) {\n console.log(logEntry);\n}\n```", + perLanguage: { + go: { + method: 'client.Logs.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Logs.List(context.TODO(), ark.LogListParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/logs \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'logs.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.logs.list()\npage = page.data[0]\nprint(page.context)', + }, + ruby: { + method: 'logs.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.logs.list\n\nputs(page)', + }, + typescript: { + method: 'client.logs.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const logEntry of client.logs.list()) {\n console.log(logEntry.context);\n}", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/logs/{requestId}', + httpMethod: 'get', + summary: 'Get API request log details', + description: + 'Retrieve detailed information about a specific API request log,\nincluding the full request and response bodies.\n\n**Body decryption:** Request and response bodies are stored encrypted\nand automatically decrypted when retrieved. Bodies larger than 25KB\nare truncated at storage time with a `... [truncated]` marker.\n\n**Use cases:**\n- Debug a specific failed request\n- Review the exact payload sent/received\n- Share request details with support\n\n**Related endpoints:**\n- `GET /logs` - List logs with filters\n', + stainlessPath: '(resource) logs > (method) retrieve', + qualified: 'client.logs.retrieve', + params: ['requestId: string;'], + response: '{ data: object; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.logs.retrieve(requestId: string): { data: log_entry_detail; meta: api_meta; success: true; }`\n\n**get** `/logs/{requestId}`\n\nRetrieve detailed information about a specific API request log,\nincluding the full request and response bodies.\n\n**Body decryption:** Request and response bodies are stored encrypted\nand automatically decrypted when retrieved. Bodies larger than 25KB\nare truncated at storage time with a `... [truncated]` marker.\n\n**Use cases:**\n- Debug a specific failed request\n- Review the exact payload sent/received\n- Share request details with support\n\n**Related endpoints:**\n- `GET /logs` - List logs with filters\n\n\n### Parameters\n\n- `requestId: string`\n\n### Returns\n\n- `{ data: object; meta: { requestId: string; }; success: true; }`\n Detailed API request log with request/response bodies\n\n - `data: { context: { idempotencyKey?: string; ipAddress?: string; queryParams?: object; userAgent?: string; }; credential: { id: string; keyPrefix?: string; }; durationMs: number; endpoint: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; rateLimit: { limit?: number; limited?: boolean; remaining?: number; reset?: number; }; requestId: string; statusCode: number; timestamp: string; email?: { id?: string; recipientCount?: number; }; error?: { code?: string; message?: string; }; sdk?: { name?: string; version?: string; }; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst log = await client.logs.retrieve('req_V8GGcdWYzgeWIHiI');\n\nconsole.log(log);\n```", + perLanguage: { + go: { + method: 'client.Logs.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tlog, err := client.Logs.Get(context.TODO(), "req_V8GGcdWYzgeWIHiI")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", log.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/logs/$REQUEST_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'logs.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nlog = client.logs.retrieve(\n "req_V8GGcdWYzgeWIHiI",\n)\nprint(log.data)', + }, + ruby: { + method: 'logs.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nlog = ark.logs.retrieve("req_V8GGcdWYzgeWIHiI")\n\nputs(log)', + }, + typescript: { + method: 'client.logs.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst log = await client.logs.retrieve('req_V8GGcdWYzgeWIHiI');\n\nconsole.log(log.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/usage', + httpMethod: 'get', + summary: 'Get org-wide usage summary', + description: + 'Returns aggregated email sending statistics for your entire organization.\nFor per-tenant breakdown, use `GET /usage/tenants`.\n\n**Use cases:**\n- Platform dashboards showing org-wide metrics\n- Quick health check on overall sending\n- Monitoring total volume and delivery rates\n\n**Response includes:**\n- `emails` - Aggregated email counts across all tenants\n- `rates` - Overall delivery and bounce rates\n- `tenants` - Tenant count summary (total, active, with activity)\n\n**Related endpoints:**\n- `GET /usage/tenants` - Paginated usage per tenant\n- `GET /usage/export` - Export usage data for billing\n- `GET /tenants/{tenantId}/usage` - Single tenant usage details\n- `GET /limits` - Rate limits and send limits\n', + stainlessPath: '(resource) usage > (method) retrieve', + qualified: 'client.usage.retrieve', + params: ['period?: string;', 'timezone?: string;'], + response: + '{ data: { emails: object; period: object; rates: object; tenants: { active: number; total: number; withActivity: number; }; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.usage.retrieve(period?: string, timezone?: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/usage`\n\nReturns aggregated email sending statistics for your entire organization.\nFor per-tenant breakdown, use `GET /usage/tenants`.\n\n**Use cases:**\n- Platform dashboards showing org-wide metrics\n- Quick health check on overall sending\n- Monitoring total volume and delivery rates\n\n**Response includes:**\n- `emails` - Aggregated email counts across all tenants\n- `rates` - Overall delivery and bounce rates\n- `tenants` - Tenant count summary (total, active, with activity)\n\n**Related endpoints:**\n- `GET /usage/tenants` - Paginated usage per tenant\n- `GET /usage/export` - Export usage data for billing\n- `GET /tenants/{tenantId}/usage` - Single tenant usage details\n- `GET /limits` - Rate limits and send limits\n\n\n### Parameters\n\n- `period?: string`\n Time period for usage data.\n\n**Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`,\n`this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_90_days`\n\n**Month format:** `2024-01` (YYYY-MM)\n\n**Custom range:** `2024-01-01..2024-01-15`\n\n\n- `timezone?: string`\n Timezone for period calculations (IANA format)\n\n### Returns\n\n- `{ data: { emails: object; period: object; rates: object; tenants: { active: number; total: number; withActivity: number; }; }; meta: { requestId: string; }; success: true; }`\n Org-wide usage summary response\n\n - `data: { emails: { bounced: number; delivered: number; hard_failed: number; held: number; sent: number; soft_failed: number; }; period: { end: string; start: string; }; rates: { bounce_rate: number; delivery_rate: number; }; tenants: { active: number; total: number; withActivity: number; }; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst orgUsageSummary = await client.usage.retrieve();\n\nconsole.log(orgUsageSummary);\n```", + perLanguage: { + go: { + method: 'client.Usage.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\torgUsageSummary, err := client.Usage.Get(context.TODO(), ark.UsageGetParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", orgUsageSummary.Data)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/usage \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'usage.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\norg_usage_summary = client.usage.retrieve()\nprint(org_usage_summary.data)', + }, + ruby: { + method: 'usage.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\norg_usage_summary = ark.usage.retrieve\n\nputs(org_usage_summary)', + }, + typescript: { + method: 'client.usage.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst orgUsageSummary = await client.usage.retrieve();\n\nconsole.log(orgUsageSummary.data);", + }, + }, + }, + { + name: 'list_tenants', + endpoint: '/usage/tenants', + httpMethod: 'get', + summary: 'List usage for all tenants', + description: + 'Returns email usage statistics for all tenants in your organization.\nResults are paginated with page-based navigation.\n\n**Jobs to be done:**\n- Generate monthly billing invoices per tenant\n- Build admin dashboards showing all customer usage\n- Identify high-volume or problematic tenants\n- Track usage against plan limits\n\n**Sorting options:**\n- `sent`, `-sent` - Sort by emails sent (ascending/descending)\n- `delivered`, `-delivered` - Sort by emails delivered\n- `bounce_rate`, `-bounce_rate` - Sort by bounce rate\n- `tenant_name`, `-tenant_name` - Sort alphabetically by tenant name\n\n**Filtering:**\n- `status` - Filter by tenant status (active, suspended, archived)\n- `minSent` - Only include tenants with at least N emails sent\n\n**Auto-pagination:** SDKs support iterating over all pages automatically.\n', + stainlessPath: '(resource) usage > (method) list_tenants', + qualified: 'client.usage.listTenants', + params: [ + 'minSent?: number;', + 'page?: number;', + 'period?: string;', + 'perPage?: number;', + 'sort?: string;', + "status?: 'active' | 'suspended' | 'archived';", + 'timezone?: string;', + ], + response: + "{ emails: { bounced: number; delivered: number; hard_failed: number; held: number; sent: number; soft_failed: number; }; rates: { bounce_rate: number; delivery_rate: number; }; status: 'active' | 'suspended' | 'archived'; tenantId: string; tenantName: string; externalId?: string; }", + markdown: + "## list_tenants\n\n`client.usage.listTenants(minSent?: number, page?: number, period?: string, perPage?: number, sort?: string, status?: 'active' | 'suspended' | 'archived', timezone?: string): { emails: email_counts; rates: email_rates; status: 'active' | 'suspended' | 'archived'; tenantId: string; tenantName: string; externalId?: string; }`\n\n**get** `/usage/tenants`\n\nReturns email usage statistics for all tenants in your organization.\nResults are paginated with page-based navigation.\n\n**Jobs to be done:**\n- Generate monthly billing invoices per tenant\n- Build admin dashboards showing all customer usage\n- Identify high-volume or problematic tenants\n- Track usage against plan limits\n\n**Sorting options:**\n- `sent`, `-sent` - Sort by emails sent (ascending/descending)\n- `delivered`, `-delivered` - Sort by emails delivered\n- `bounce_rate`, `-bounce_rate` - Sort by bounce rate\n- `tenant_name`, `-tenant_name` - Sort alphabetically by tenant name\n\n**Filtering:**\n- `status` - Filter by tenant status (active, suspended, archived)\n- `minSent` - Only include tenants with at least N emails sent\n\n**Auto-pagination:** SDKs support iterating over all pages automatically.\n\n\n### Parameters\n\n- `minSent?: number`\n Only include tenants with at least this many emails sent\n\n- `page?: number`\n Page number (1-indexed)\n\n- `period?: string`\n Time period for usage data. Defaults to current month.\n\n**Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`,\n`this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_90_days`\n\n**Month format:** `2024-01` (YYYY-MM)\n\n**Custom range:** `2024-01-01..2024-01-15`\n\n\n- `perPage?: number`\n Results per page (max 100)\n\n- `sort?: string`\n Sort order for results. Prefix with `-` for descending order.\n\n- `status?: 'active' | 'suspended' | 'archived'`\n Filter by tenant status\n\n- `timezone?: string`\n Timezone for period calculations (IANA format). Defaults to UTC.\n\n### Returns\n\n- `{ emails: { bounced: number; delivered: number; hard_failed: number; held: number; sent: number; soft_failed: number; }; rates: { bounce_rate: number; delivery_rate: number; }; status: 'active' | 'suspended' | 'archived'; tenantId: string; tenantName: string; externalId?: string; }`\n Usage record for a single tenant (camelCase for SDK)\n\n - `emails: { bounced: number; delivered: number; hard_failed: number; held: number; sent: number; soft_failed: number; }`\n - `rates: { bounce_rate: number; delivery_rate: number; }`\n - `status: 'active' | 'suspended' | 'archived'`\n - `tenantId: string`\n - `tenantName: string`\n - `externalId?: string`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const tenantUsageItem of client.usage.listTenants()) {\n console.log(tenantUsageItem);\n}\n```", + perLanguage: { + go: { + method: 'client.Usage.ListTenants', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Usage.ListTenants(context.TODO(), ark.UsageListTenantsParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/usage/tenants \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'usage.list_tenants', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.usage.list_tenants()\npage = page.data[0]\nprint(page.emails)', + }, + ruby: { + method: 'usage.list_tenants', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.usage.list_tenants\n\nputs(page)', + }, + typescript: { + method: 'client.usage.listTenants', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const tenantUsageItem of client.usage.listTenants()) {\n console.log(tenantUsageItem.emails);\n}", + }, + }, + }, + { + name: 'export', + endpoint: '/usage/export', + httpMethod: 'get', + summary: 'Export tenant usage data', + description: + 'Export email usage data for all tenants in CSV or JSON Lines format.\nDesigned for billing system integration, data warehousing, and analytics.\n\n**Jobs to be done:**\n- Import usage data into billing systems (Stripe, Chargebee, etc.)\n- Load into data warehouses (Snowflake, BigQuery, etc.)\n- Process in spreadsheets (Excel, Google Sheets)\n- Feed into BI tools (Looker, Metabase, etc.)\n\n**Export formats:**\n- `csv` - UTF-8 with BOM for Excel compatibility (default)\n- `jsonl` - JSON Lines (one JSON object per line, streamable)\n\n**CSV columns:**\n`tenant_id`, `tenant_name`, `external_id`, `status`, `sent`, `delivered`,\n`soft_failed`, `hard_failed`, `bounced`, `held`, `delivery_rate`,\n`bounce_rate`, `period_start`, `period_end`\n\n**Response headers:**\n- `Content-Disposition` - Filename for download\n- `Content-Type` - `text/csv` or `application/x-ndjson`\n', + stainlessPath: '(resource) usage > (method) export', + qualified: 'client.usage.export', + params: [ + "format?: 'csv' | 'jsonl';", + 'minSent?: number;', + 'period?: string;', + "status?: 'active' | 'suspended' | 'archived';", + 'timezone?: string;', + ], + response: + "{ bounce_rate: number; bounced: number; delivered: number; delivery_rate: number; hard_failed: number; held: number; sent: number; soft_failed: number; status: 'active' | 'suspended' | 'archived'; tenant_id: string; tenant_name: string; external_id?: string; }[]", + markdown: + "## export\n\n`client.usage.export(format?: 'csv' | 'jsonl', minSent?: number, period?: string, status?: 'active' | 'suspended' | 'archived', timezone?: string): { bounce_rate: number; bounced: number; delivered: number; delivery_rate: number; hard_failed: number; held: number; sent: number; soft_failed: number; status: 'active' | 'suspended' | 'archived'; tenant_id: string; tenant_name: string; external_id?: string; }[]`\n\n**get** `/usage/export`\n\nExport email usage data for all tenants in CSV or JSON Lines format.\nDesigned for billing system integration, data warehousing, and analytics.\n\n**Jobs to be done:**\n- Import usage data into billing systems (Stripe, Chargebee, etc.)\n- Load into data warehouses (Snowflake, BigQuery, etc.)\n- Process in spreadsheets (Excel, Google Sheets)\n- Feed into BI tools (Looker, Metabase, etc.)\n\n**Export formats:**\n- `csv` - UTF-8 with BOM for Excel compatibility (default)\n- `jsonl` - JSON Lines (one JSON object per line, streamable)\n\n**CSV columns:**\n`tenant_id`, `tenant_name`, `external_id`, `status`, `sent`, `delivered`,\n`soft_failed`, `hard_failed`, `bounced`, `held`, `delivery_rate`,\n`bounce_rate`, `period_start`, `period_end`\n\n**Response headers:**\n- `Content-Disposition` - Filename for download\n- `Content-Type` - `text/csv` or `application/x-ndjson`\n\n\n### Parameters\n\n- `format?: 'csv' | 'jsonl'`\n Export format\n\n- `minSent?: number`\n Only include tenants with at least this many emails sent\n\n- `period?: string`\n Time period for export.\n\n**Shortcuts:** `this_month`, `last_month`, `last_30_days`, etc.\n\n**Month format:** `2024-01` (YYYY-MM)\n\n**Custom range:** `2024-01-01..2024-01-15`\n\n\n- `status?: 'active' | 'suspended' | 'archived'`\n Filter by tenant status\n\n- `timezone?: string`\n Timezone for period calculations (IANA format)\n\n### Returns\n\n- `{ bounce_rate: number; bounced: number; delivered: number; delivery_rate: number; hard_failed: number; held: number; sent: number; soft_failed: number; status: 'active' | 'suspended' | 'archived'; tenant_id: string; tenant_name: string; external_id?: string; }[]`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.usage.export();\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Usage.Export', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Usage.Export(context.TODO(), ark.UsageExportParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/usage/export \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'usage.export', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.usage.export()\nprint(response)', + }, + ruby: { + method: 'usage.export', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.usage.export\n\nputs(response)', + }, + typescript: { + method: 'client.usage.export', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.usage.export();\n\nconsole.log(response);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/limits', + httpMethod: 'get', + summary: 'Get account rate limits and send limits', + description: + "Returns current rate limit and send limit information for your account.\n\nThis endpoint is the recommended way to check your account's operational limits.\nUse `/usage` endpoints for historical usage analytics.\n\n**Response includes:**\n- `rateLimit` - API request rate limit (requests per second)\n- `sendLimit` - Email sending limit (emails per hour)\n- `billing` - Credit balance and auto-recharge configuration\n\n**Notes:**\n- This request counts against your rate limit\n- `sendLimit` may be null if the service is temporarily unavailable\n- `billing` is null if billing is not configured\n- Send limit resets at the top of each hour\n", + stainlessPath: '(resource) limits > (method) retrieve', + qualified: 'client.limits.retrieve', + response: + '{ data: { billing: object; rateLimit: object; sendLimit: object; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.limits.retrieve(): { data: limits_data; meta: api_meta; success: true; }`\n\n**get** `/limits`\n\nReturns current rate limit and send limit information for your account.\n\nThis endpoint is the recommended way to check your account's operational limits.\nUse `/usage` endpoints for historical usage analytics.\n\n**Response includes:**\n- `rateLimit` - API request rate limit (requests per second)\n- `sendLimit` - Email sending limit (emails per hour)\n- `billing` - Credit balance and auto-recharge configuration\n\n**Notes:**\n- This request counts against your rate limit\n- `sendLimit` may be null if the service is temporarily unavailable\n- `billing` is null if billing is not configured\n- Send limit resets at the top of each hour\n\n\n### Returns\n\n- `{ data: { billing: object; rateLimit: object; sendLimit: object; }; meta: { requestId: string; }; success: true; }`\n Account rate limits and send limits response\n\n - `data: { billing: { autoRecharge: { amount: string; enabled: boolean; threshold: string; }; creditBalance: string; creditBalanceCents: number; hasPaymentMethod: boolean; }; rateLimit: { limit: number; period: 'second'; remaining: number; reset: number; }; sendLimit: { approaching: boolean; exceeded: boolean; limit: number; period: 'hour'; remaining: number; resetsAt: string; usagePercent: number; used: number; }; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst limit = await client.limits.retrieve();\n\nconsole.log(limit);\n```", + perLanguage: { + go: { + method: 'client.Limits.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tlimit, err := client.Limits.Get(context.TODO())\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", limit.Data)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/limits \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'limits.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nlimit = client.limits.retrieve()\nprint(limit.data)', + }, + ruby: { + method: 'limits.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nlimit = ark.limits.retrieve\n\nputs(limit)', + }, + typescript: { + method: 'client.limits.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst limit = await client.limits.retrieve();\n\nconsole.log(limit.data);", + }, + }, + }, + { + name: 'create', + endpoint: '/tenants', + httpMethod: 'post', + summary: 'Create a tenant', + description: + 'Create a new tenant.\n\nReturns the created tenant with a unique `id`. Store this ID in your database\nto reference this tenant later.\n', + stainlessPath: '(resource) tenants > (method) create', + qualified: 'client.tenants.create', + params: ['name: string;', 'metadata?: object;'], + response: + "{ data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## create\n\n`client.tenants.create(name: string, metadata?: object): { data: tenant; meta: api_meta; success: true; }`\n\n**post** `/tenants`\n\nCreate a new tenant.\n\nReturns the created tenant with a unique `id`. Store this ID in your database\nto reference this tenant later.\n\n\n### Parameters\n\n- `name: string`\n Display name for the tenant (e.g., your customer's company name)\n\n- `metadata?: object`\n Custom key-value pairs. Useful for storing references to your internal systems.\n\n**Limits:**\n- Max 50 keys\n- Key names max 40 characters\n- String values max 500 characters\n- Total size max 8KB\n\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tenant = await client.tenants.create({ name: 'Acme Corp' });\n\nconsole.log(tenant);\n```", + perLanguage: { + go: { + method: 'client.Tenants.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttenant, err := client.Tenants.New(context.TODO(), ark.TenantNewParams{\n\t\tName: "Acme Corp",\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tenant.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "name": "Acme Corp",\n "metadata": {\n "plan": "pro",\n "internalId": "cust_12345",\n "region": "us-west"\n }\n }\'', + }, + python: { + method: 'tenants.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntenant = client.tenants.create(\n name="Acme Corp",\n)\nprint(tenant.data)', + }, + ruby: { + method: 'tenants.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntenant = ark.tenants.create(name: "Acme Corp")\n\nputs(tenant)', + }, + typescript: { + method: 'client.tenants.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tenant = await client.tenants.create({ name: 'Acme Corp' });\n\nconsole.log(tenant.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/tenants', + httpMethod: 'get', + summary: 'List all tenants', + description: 'List all tenants with pagination. Filter by `status` if needed.\n', + stainlessPath: '(resource) tenants > (method) list', + qualified: 'client.tenants.list', + params: ['page?: number;', 'perPage?: number;', "status?: 'active' | 'suspended' | 'archived';"], + response: + "{ id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }", + markdown: + "## list\n\n`client.tenants.list(page?: number, perPage?: number, status?: 'active' | 'suspended' | 'archived'): { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }`\n\n**get** `/tenants`\n\nList all tenants with pagination. Filter by `status` if needed.\n\n\n### Parameters\n\n- `page?: number`\n Page number (1-indexed)\n\n- `perPage?: number`\n Number of items per page (max 100)\n\n- `status?: 'active' | 'suspended' | 'archived'`\n Filter by tenant status\n\n### Returns\n\n- `{ id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }`\n\n - `id: string`\n - `createdAt: string`\n - `metadata: object`\n - `name: string`\n - `status: 'active' | 'suspended' | 'archived'`\n - `updatedAt: string`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const tenant of client.tenants.list()) {\n console.log(tenant);\n}\n```", + perLanguage: { + go: { + method: 'client.Tenants.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Tenants.List(context.TODO(), ark.TenantListParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: 'curl https://api.arkhq.io/v1/tenants \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.tenants.list()\npage = page.data[0]\nprint(page.id)', + }, + ruby: { + method: 'tenants.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.tenants.list\n\nputs(page)', + }, + typescript: { + method: 'client.tenants.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const tenant of client.tenants.list()) {\n console.log(tenant.id);\n}", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}', + httpMethod: 'get', + summary: 'Get a tenant', + description: 'Get a tenant by ID.', + stainlessPath: '(resource) tenants > (method) retrieve', + qualified: 'client.tenants.retrieve', + params: ['tenantId: string;'], + response: + "{ data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## retrieve\n\n`client.tenants.retrieve(tenantId: string): { data: tenant; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}`\n\nGet a tenant by ID.\n\n### Parameters\n\n- `tenantId: string`\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tenant = await client.tenants.retrieve('cm6abc123def456');\n\nconsole.log(tenant);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttenant, err := client.Tenants.Get(context.TODO(), "cm6abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tenant.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntenant = client.tenants.retrieve(\n "cm6abc123def456",\n)\nprint(tenant.data)', + }, + ruby: { + method: 'tenants.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntenant = ark.tenants.retrieve("cm6abc123def456")\n\nputs(tenant)', + }, + typescript: { + method: 'client.tenants.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tenant = await client.tenants.retrieve('cm6abc123def456');\n\nconsole.log(tenant.data);", + }, + }, + }, + { + name: 'update', + endpoint: '/tenants/{tenantId}', + httpMethod: 'patch', + summary: 'Update a tenant', + description: + "Update a tenant's name, metadata, or status. At least one field is required.\n\nMetadata is replaced entirely—include all keys you want to keep.\n", + stainlessPath: '(resource) tenants > (method) update', + qualified: 'client.tenants.update', + params: [ + 'tenantId: string;', + 'metadata?: object;', + 'name?: string;', + "status?: 'active' | 'suspended' | 'archived';", + ], + response: + "{ data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## update\n\n`client.tenants.update(tenantId: string, metadata?: object, name?: string, status?: 'active' | 'suspended' | 'archived'): { data: tenant; meta: api_meta; success: true; }`\n\n**patch** `/tenants/{tenantId}`\n\nUpdate a tenant's name, metadata, or status. At least one field is required.\n\nMetadata is replaced entirely—include all keys you want to keep.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `metadata?: object`\n Custom key-value pairs. Useful for storing references to your internal systems.\n\n**Limits:**\n- Max 50 keys\n- Key names max 40 characters\n- String values max 500 characters\n- Total size max 8KB\n\n\n- `name?: string`\n Display name for the tenant\n\n- `status?: 'active' | 'suspended' | 'archived'`\n Tenant status\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; metadata: object; name: string; status: 'active' | 'suspended' | 'archived'; updatedAt: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tenant = await client.tenants.update('cm6abc123def456');\n\nconsole.log(tenant);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Update', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttenant, err := client.Tenants.Update(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantUpdateParams{\n\t\t\tName: ark.String("Acme Corporation"),\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tenant.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID \\\n -X PATCH \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "metadata": {\n "plan": "pro",\n "internalId": "cust_12345",\n "region": "us-west"\n },\n "name": "Acme Corporation",\n "status": "active"\n }\'', + }, + python: { + method: 'tenants.update', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntenant = client.tenants.update(\n tenant_id="cm6abc123def456",\n name="Acme Corporation",\n)\nprint(tenant.data)', + }, + ruby: { + method: 'tenants.update', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntenant = ark.tenants.update("cm6abc123def456")\n\nputs(tenant)', + }, + typescript: { + method: 'client.tenants.update', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tenant = await client.tenants.update('cm6abc123def456', { name: 'Acme Corporation' });\n\nconsole.log(tenant.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/tenants/{tenantId}', + httpMethod: 'delete', + summary: 'Delete a tenant', + description: 'Permanently delete a tenant. This cannot be undone.\n', + stainlessPath: '(resource) tenants > (method) delete', + qualified: 'client.tenants.delete', + params: ['tenantId: string;'], + response: '{ data: { deleted: true; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.tenants.delete(tenantId: string): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/tenants/{tenantId}`\n\nPermanently delete a tenant. This cannot be undone.\n\n\n### Parameters\n\n- `tenantId: string`\n\n### Returns\n\n- `{ data: { deleted: true; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { deleted: true; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tenant = await client.tenants.delete('cm6abc123def456');\n\nconsole.log(tenant);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttenant, err := client.Tenants.Delete(context.TODO(), "cm6abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tenant.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntenant = client.tenants.delete(\n "cm6abc123def456",\n)\nprint(tenant.data)', + }, + ruby: { + method: 'tenants.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntenant = ark.tenants.delete("cm6abc123def456")\n\nputs(tenant)', + }, + typescript: { + method: 'client.tenants.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tenant = await client.tenants.delete('cm6abc123def456');\n\nconsole.log(tenant.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/tenants/{tenantId}/credentials', + httpMethod: 'get', + summary: 'List credentials for a tenant', + description: + 'List all SMTP and API credentials for a tenant. Credentials are used to send\nemails via Ark on behalf of the tenant.\n\n**Security:** Credential keys are not returned in the list response. Use the\nretrieve endpoint with `reveal=true` to get the key.\n', + stainlessPath: '(resource) tenants.credentials > (method) list', + qualified: 'client.tenants.credentials.list', + params: ['tenantId: string;', 'page?: number;', 'perPage?: number;', "type?: 'smtp' | 'api';"], + response: + "{ id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; }", + markdown: + "## list\n\n`client.tenants.credentials.list(tenantId: string, page?: number, perPage?: number, type?: 'smtp' | 'api'): { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; }`\n\n**get** `/tenants/{tenantId}/credentials`\n\nList all SMTP and API credentials for a tenant. Credentials are used to send\nemails via Ark on behalf of the tenant.\n\n**Security:** Credential keys are not returned in the list response. Use the\nretrieve endpoint with `reveal=true` to get the key.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `page?: number`\n Page number (1-indexed)\n\n- `perPage?: number`\n Number of items per page (max 100)\n\n- `type?: 'smtp' | 'api'`\n Filter by credential type\n\n### Returns\n\n- `{ id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; }`\n\n - `id: number`\n - `createdAt: string`\n - `hold: boolean`\n - `lastUsedAt: string`\n - `name: string`\n - `type: 'smtp' | 'api'`\n - `updatedAt: string`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const credentialListResponse of client.tenants.credentials.list('cm6abc123def456')) {\n console.log(credentialListResponse);\n}\n```", + perLanguage: { + go: { + method: 'client.Tenants.Credentials.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Tenants.Credentials.List(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantCredentialListParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/credentials \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.credentials.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.tenants.credentials.list(\n tenant_id="cm6abc123def456",\n)\npage = page.data[0]\nprint(page.id)', + }, + ruby: { + method: 'tenants.credentials.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.tenants.credentials.list("cm6abc123def456")\n\nputs(page)', + }, + typescript: { + method: 'client.tenants.credentials.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const credentialListResponse of client.tenants.credentials.list('cm6abc123def456')) {\n console.log(credentialListResponse.id);\n}", + }, + }, + }, + { + name: 'create', + endpoint: '/tenants/{tenantId}/credentials', + httpMethod: 'post', + summary: 'Create a credential for a tenant', + description: + 'Create a new SMTP or API credential for a tenant. The credential can be used\nto send emails via Ark on behalf of the tenant.\n\n**Important:** The credential key is only returned once at creation time.\nStore it securely - you cannot retrieve it again.\n\n**Credential Types:**\n- `smtp` - For SMTP-based email sending. Returns both `key` and `smtpUsername`.\n- `api` - For API-based email sending. Returns only `key`.\n', + stainlessPath: '(resource) tenants.credentials > (method) create', + qualified: 'client.tenants.credentials.create', + params: ['tenantId: string;', 'name: string;', "type: 'smtp' | 'api';"], + response: + "{ data: { id: number; createdAt: string; hold: boolean; key: string; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; smtpUsername?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## create\n\n`client.tenants.credentials.create(tenantId: string, name: string, type: 'smtp' | 'api'): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/credentials`\n\nCreate a new SMTP or API credential for a tenant. The credential can be used\nto send emails via Ark on behalf of the tenant.\n\n**Important:** The credential key is only returned once at creation time.\nStore it securely - you cannot retrieve it again.\n\n**Credential Types:**\n- `smtp` - For SMTP-based email sending. Returns both `key` and `smtpUsername`.\n- `api` - For API-based email sending. Returns only `key`.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `name: string`\n Name for the credential. Can only contain letters, numbers,\nhyphens, and underscores. Max 50 characters.\n\n\n- `type: 'smtp' | 'api'`\n Type of credential:\n- `smtp` - For SMTP-based email sending\n- `api` - For API-based email sending\n\n### Returns\n\n- `{ data: { id: number; createdAt: string; hold: boolean; key: string; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; smtpUsername?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: number; createdAt: string; hold: boolean; key: string; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; smtpUsername?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst credential = await client.tenants.credentials.create('cm6abc123def456', { name: 'production-smtp', type: 'smtp' });\n\nconsole.log(credential);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Credentials.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tcredential, err := client.Tenants.Credentials.New(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantCredentialNewParams{\n\t\t\tName: "production-smtp",\n\t\t\tType: ark.TenantCredentialNewParamsTypeSmtp,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", credential.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/credentials \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "name": "production-smtp",\n "type": "smtp"\n }\'', + }, + python: { + method: 'tenants.credentials.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ncredential = client.tenants.credentials.create(\n tenant_id="cm6abc123def456",\n name="production-smtp",\n type="smtp",\n)\nprint(credential.data)', + }, + ruby: { + method: 'tenants.credentials.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ncredential = ark.tenants.credentials.create("cm6abc123def456", name: "production-smtp", type: :smtp)\n\nputs(credential)', + }, + typescript: { + method: 'client.tenants.credentials.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst credential = await client.tenants.credentials.create('cm6abc123def456', {\n name: 'production-smtp',\n type: 'smtp',\n});\n\nconsole.log(credential.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}/credentials/{credentialId}', + httpMethod: 'get', + summary: 'Get a credential', + description: + 'Get details of a specific credential.\n\n**Revealing the key:** By default, the credential key is not returned.\nPass `reveal=true` to include the key in the response. Use this sparingly\nand only when you need to retrieve the key (e.g., for configuration).\n', + stainlessPath: '(resource) tenants.credentials > (method) retrieve', + qualified: 'client.tenants.credentials.retrieve', + params: ['tenantId: string;', 'credentialId: number;', 'reveal?: boolean;'], + response: + "{ data: { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; key?: string; smtpUsername?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## retrieve\n\n`client.tenants.credentials.retrieve(tenantId: string, credentialId: number, reveal?: boolean): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/credentials/{credentialId}`\n\nGet details of a specific credential.\n\n**Revealing the key:** By default, the credential key is not returned.\nPass `reveal=true` to include the key in the response. Use this sparingly\nand only when you need to retrieve the key (e.g., for configuration).\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `credentialId: number`\n\n- `reveal?: boolean`\n Set to `true` to include the credential key in the response\n\n### Returns\n\n- `{ data: { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; key?: string; smtpUsername?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; key?: string; smtpUsername?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst credential = await client.tenants.credentials.retrieve(123, { tenantId: 'cm6abc123def456' });\n\nconsole.log(credential);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Credentials.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tcredential, err := client.Tenants.Credentials.Get(\n\t\tcontext.TODO(),\n\t\t123,\n\t\tark.TenantCredentialGetParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", credential.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/credentials/$CREDENTIAL_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.credentials.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ncredential = client.tenants.credentials.retrieve(\n credential_id=123,\n tenant_id="cm6abc123def456",\n)\nprint(credential.data)', + }, + ruby: { + method: 'tenants.credentials.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ncredential = ark.tenants.credentials.retrieve(123, tenant_id: "cm6abc123def456")\n\nputs(credential)', + }, + typescript: { + method: 'client.tenants.credentials.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst credential = await client.tenants.credentials.retrieve(123, { tenantId: 'cm6abc123def456' });\n\nconsole.log(credential.data);", + }, + }, + }, + { + name: 'update', + endpoint: '/tenants/{tenantId}/credentials/{credentialId}', + httpMethod: 'patch', + summary: 'Update a credential', + description: + "Update a credential's name or hold status.\n\n**Hold Status:**\n- When `hold: true`, the credential is disabled and cannot be used to send emails.\n- When `hold: false`, the credential is active and can send emails.\n- Use this to temporarily disable a credential without deleting it.\n", + stainlessPath: '(resource) tenants.credentials > (method) update', + qualified: 'client.tenants.credentials.update', + params: ['tenantId: string;', 'credentialId: number;', 'hold?: boolean;', 'name?: string;'], + response: + "{ data: { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; key?: string; smtpUsername?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## update\n\n`client.tenants.credentials.update(tenantId: string, credentialId: number, hold?: boolean, name?: string): { data: object; meta: api_meta; success: true; }`\n\n**patch** `/tenants/{tenantId}/credentials/{credentialId}`\n\nUpdate a credential's name or hold status.\n\n**Hold Status:**\n- When `hold: true`, the credential is disabled and cannot be used to send emails.\n- When `hold: false`, the credential is active and can send emails.\n- Use this to temporarily disable a credential without deleting it.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `credentialId: number`\n\n- `hold?: boolean`\n Set to `true` to disable the credential (put on hold).\nSet to `false` to enable the credential (release from hold).\n\n\n- `name?: string`\n New name for the credential\n\n### Returns\n\n- `{ data: { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; key?: string; smtpUsername?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: number; createdAt: string; hold: boolean; lastUsedAt: string; name: string; type: 'smtp' | 'api'; updatedAt: string; key?: string; smtpUsername?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst credential = await client.tenants.credentials.update(123, { tenantId: 'cm6abc123def456' });\n\nconsole.log(credential);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Credentials.Update', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tcredential, err := client.Tenants.Credentials.Update(\n\t\tcontext.TODO(),\n\t\t123,\n\t\tark.TenantCredentialUpdateParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t\tName: ark.String("production-smtp-v2"),\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", credential.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/credentials/$CREDENTIAL_ID \\\n -X PATCH \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "hold": true,\n "name": "production-smtp-v2"\n }\'', + }, + python: { + method: 'tenants.credentials.update', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ncredential = client.tenants.credentials.update(\n credential_id=123,\n tenant_id="cm6abc123def456",\n name="production-smtp-v2",\n)\nprint(credential.data)', + }, + ruby: { + method: 'tenants.credentials.update', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ncredential = ark.tenants.credentials.update(123, tenant_id: "cm6abc123def456")\n\nputs(credential)', + }, + typescript: { + method: 'client.tenants.credentials.update', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst credential = await client.tenants.credentials.update(123, {\n tenantId: 'cm6abc123def456',\n name: 'production-smtp-v2',\n});\n\nconsole.log(credential.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/tenants/{tenantId}/credentials/{credentialId}', + httpMethod: 'delete', + summary: 'Delete a credential', + description: + 'Permanently delete (revoke) a credential. The credential can no longer be\nused to send emails.\n\n**Warning:** This action is irreversible. If you want to temporarily disable\na credential, use the update endpoint to set `hold: true` instead.\n', + stainlessPath: '(resource) tenants.credentials > (method) delete', + qualified: 'client.tenants.credentials.delete', + params: ['tenantId: string;', 'credentialId: number;'], + response: '{ data: { deleted: true; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.tenants.credentials.delete(tenantId: string, credentialId: number): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/tenants/{tenantId}/credentials/{credentialId}`\n\nPermanently delete (revoke) a credential. The credential can no longer be\nused to send emails.\n\n**Warning:** This action is irreversible. If you want to temporarily disable\na credential, use the update endpoint to set `hold: true` instead.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `credentialId: number`\n\n### Returns\n\n- `{ data: { deleted: true; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { deleted: true; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst credential = await client.tenants.credentials.delete(123, { tenantId: 'cm6abc123def456' });\n\nconsole.log(credential);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Credentials.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tcredential, err := client.Tenants.Credentials.Delete(\n\t\tcontext.TODO(),\n\t\t123,\n\t\tark.TenantCredentialDeleteParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", credential.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/credentials/$CREDENTIAL_ID \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.credentials.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ncredential = client.tenants.credentials.delete(\n credential_id=123,\n tenant_id="cm6abc123def456",\n)\nprint(credential.data)', + }, + ruby: { + method: 'tenants.credentials.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ncredential = ark.tenants.credentials.delete(123, tenant_id: "cm6abc123def456")\n\nputs(credential)', + }, + typescript: { + method: 'client.tenants.credentials.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst credential = await client.tenants.credentials.delete(123, { tenantId: 'cm6abc123def456' });\n\nconsole.log(credential.data);", + }, + }, + }, + { + name: 'create', + endpoint: '/tenants/{tenantId}/domains', + httpMethod: 'post', + summary: 'Add a sending domain', + description: + 'Add a new sending domain to a tenant. Returns DNS records that must\nbe configured before the domain can be verified.\n\nEach tenant gets their own isolated mail server for domain isolation.\n\n**Required DNS records:**\n- **SPF** - TXT record for sender authentication\n- **DKIM** - TXT record for email signing\n- **Return Path** - CNAME for bounce handling\n\nAfter adding DNS records, call `POST /tenants/{tenantId}/domains/{domainId}/verify` to verify.\n', + stainlessPath: '(resource) tenants.domains > (method) create', + qualified: 'client.tenants.domains.create', + params: ['tenantId: string;', 'name: string;'], + response: + '{ data: { id: number; createdAt: string; dnsRecords: { dkim?: dns_record; returnPath?: dns_record; spf?: dns_record; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## create\n\n`client.tenants.domains.create(tenantId: string, name: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/domains`\n\nAdd a new sending domain to a tenant. Returns DNS records that must\nbe configured before the domain can be verified.\n\nEach tenant gets their own isolated mail server for domain isolation.\n\n**Required DNS records:**\n- **SPF** - TXT record for sender authentication\n- **DKIM** - TXT record for email signing\n- **Return Path** - CNAME for bounce handling\n\nAfter adding DNS records, call `POST /tenants/{tenantId}/domains/{domainId}/verify` to verify.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `name: string`\n Domain name (e.g., \"mail.example.com\")\n\n### Returns\n\n- `{ data: { id: number; createdAt: string; dnsRecords: { dkim?: dns_record; returnPath?: dns_record; spf?: dns_record; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: number; createdAt: string; dnsRecords: { dkim?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; returnPath?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; spf?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst domain = await client.tenants.domains.create('cm6abc123def456', { name: 'notifications.myapp.com' });\n\nconsole.log(domain);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Domains.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tdomain, err := client.Tenants.Domains.New(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantDomainNewParams{\n\t\t\tName: "notifications.myapp.com",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", domain.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/domains \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "name": "notifications.myapp.com"\n }\'', + }, + python: { + method: 'tenants.domains.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ndomain = client.tenants.domains.create(\n tenant_id="cm6abc123def456",\n name="notifications.myapp.com",\n)\nprint(domain.data)', + }, + ruby: { + method: 'tenants.domains.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ndomain = ark.tenants.domains.create("cm6abc123def456", name: "notifications.myapp.com")\n\nputs(domain)', + }, + typescript: { + method: 'client.tenants.domains.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst domain = await client.tenants.domains.create('cm6abc123def456', {\n name: 'notifications.myapp.com',\n});\n\nconsole.log(domain.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/tenants/{tenantId}/domains', + httpMethod: 'get', + summary: 'List tenant domains', + description: 'Get all sending domains for a specific tenant with their verification status.\n', + stainlessPath: '(resource) tenants.domains > (method) list', + qualified: 'client.tenants.domains.list', + params: ['tenantId: string;'], + response: + '{ data: { domains: { id: number; name: string; verified: boolean; tenant_id?: string; tenant_name?: string; }[]; }; meta: { requestId: string; }; success: true; }', + markdown: + "## list\n\n`client.tenants.domains.list(tenantId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/domains`\n\nGet all sending domains for a specific tenant with their verification status.\n\n\n### Parameters\n\n- `tenantId: string`\n\n### Returns\n\n- `{ data: { domains: { id: number; name: string; verified: boolean; tenant_id?: string; tenant_name?: string; }[]; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { domains: { id: number; name: string; verified: boolean; tenant_id?: string; tenant_name?: string; }[]; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst domains = await client.tenants.domains.list('cm6abc123def456');\n\nconsole.log(domains);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Domains.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tdomains, err := client.Tenants.Domains.List(context.TODO(), "cm6abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", domains.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/domains \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.domains.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ndomains = client.tenants.domains.list(\n "cm6abc123def456",\n)\nprint(domains.data)', + }, + ruby: { + method: 'tenants.domains.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ndomains = ark.tenants.domains.list("cm6abc123def456")\n\nputs(domains)', + }, + typescript: { + method: 'client.tenants.domains.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst domains = await client.tenants.domains.list('cm6abc123def456');\n\nconsole.log(domains.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}/domains/{domainId}', + httpMethod: 'get', + summary: 'Get domain details', + description: 'Get detailed information about a domain including DNS record status.', + stainlessPath: '(resource) tenants.domains > (method) retrieve', + qualified: 'client.tenants.domains.retrieve', + params: ['tenantId: string;', 'domainId: string;'], + response: + '{ data: { id: number; createdAt: string; dnsRecords: { dkim?: dns_record; returnPath?: dns_record; spf?: dns_record; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.tenants.domains.retrieve(tenantId: string, domainId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/domains/{domainId}`\n\nGet detailed information about a domain including DNS record status.\n\n### Parameters\n\n- `tenantId: string`\n\n- `domainId: string`\n\n### Returns\n\n- `{ data: { id: number; createdAt: string; dnsRecords: { dkim?: dns_record; returnPath?: dns_record; spf?: dns_record; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: number; createdAt: string; dnsRecords: { dkim?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; returnPath?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; spf?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst domain = await client.tenants.domains.retrieve('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(domain);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Domains.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tdomain, err := client.Tenants.Domains.Get(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantDomainGetParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", domain.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/domains/$DOMAIN_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.domains.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ndomain = client.tenants.domains.retrieve(\n domain_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(domain.data)', + }, + ruby: { + method: 'tenants.domains.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ndomain = ark.tenants.domains.retrieve("123", tenant_id: "cm6abc123def456")\n\nputs(domain)', + }, + typescript: { + method: 'client.tenants.domains.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst domain = await client.tenants.domains.retrieve('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(domain.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/tenants/{tenantId}/domains/{domainId}', + httpMethod: 'delete', + summary: 'Delete a domain', + description: + 'Remove a sending domain from a tenant. You will no longer be able to send emails\nfrom this domain.\n\n**Warning:** This action cannot be undone.\n', + stainlessPath: '(resource) tenants.domains > (method) delete', + qualified: 'client.tenants.domains.delete', + params: ['tenantId: string;', 'domainId: string;'], + response: '{ data: { message: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.tenants.domains.delete(tenantId: string, domainId: string): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/tenants/{tenantId}/domains/{domainId}`\n\nRemove a sending domain from a tenant. You will no longer be able to send emails\nfrom this domain.\n\n**Warning:** This action cannot be undone.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `domainId: string`\n\n### Returns\n\n- `{ data: { message: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { message: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst domain = await client.tenants.domains.delete('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(domain);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Domains.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tdomain, err := client.Tenants.Domains.Delete(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantDomainDeleteParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", domain.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/domains/$DOMAIN_ID \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.domains.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ndomain = client.tenants.domains.delete(\n domain_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(domain.data)', + }, + ruby: { + method: 'tenants.domains.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ndomain = ark.tenants.domains.delete("123", tenant_id: "cm6abc123def456")\n\nputs(domain)', + }, + typescript: { + method: 'client.tenants.domains.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst domain = await client.tenants.domains.delete('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(domain.data);", + }, + }, + }, + { + name: 'verify', + endpoint: '/tenants/{tenantId}/domains/{domainId}/verify', + httpMethod: 'post', + summary: 'Verify domain DNS', + description: + "Check if DNS records are correctly configured and verify the domain.\nReturns the current status of each required DNS record.\n\nCall this after you've added the DNS records shown when creating the domain.\n", + stainlessPath: '(resource) tenants.domains > (method) verify', + qualified: 'client.tenants.domains.verify', + params: ['tenantId: string;', 'domainId: string;'], + response: + '{ data: { id: number; createdAt: string; dnsRecords: { dkim?: dns_record; returnPath?: dns_record; spf?: dns_record; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## verify\n\n`client.tenants.domains.verify(tenantId: string, domainId: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/domains/{domainId}/verify`\n\nCheck if DNS records are correctly configured and verify the domain.\nReturns the current status of each required DNS record.\n\nCall this after you've added the DNS records shown when creating the domain.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `domainId: string`\n\n### Returns\n\n- `{ data: { id: number; createdAt: string; dnsRecords: { dkim?: dns_record; returnPath?: dns_record; spf?: dns_record; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: number; createdAt: string; dnsRecords: { dkim?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; returnPath?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; spf?: { fullName: string; name: string; type: 'TXT' | 'CNAME' | 'MX'; value: string; status?: 'OK' | 'Missing' | 'Invalid'; }; zone?: string; }; name: string; uuid: string; verified: boolean; tenant_id?: string; tenant_name?: string; verifiedAt?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.domains.verify('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Domains.Verify', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Domains.Verify(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantDomainVerifyParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/domains/$DOMAIN_ID/verify \\\n -X POST \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.domains.verify', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.domains.verify(\n domain_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.domains.verify', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.domains.verify("123", tenant_id: "cm6abc123def456")\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.domains.verify', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.domains.verify('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'create', + endpoint: '/tenants/{tenantId}/suppressions', + httpMethod: 'post', + summary: 'Add to suppression list', + description: + "Add an email address to the tenant's suppression list. The address will\nnot receive any emails from this tenant until removed.\n", + stainlessPath: '(resource) tenants.suppressions > (method) create', + qualified: 'client.tenants.suppressions.create', + params: ['tenantId: string;', 'address: string;', 'reason?: string;'], + response: + '{ data: { id: string; address: string; createdAt: string; reason?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## create\n\n`client.tenants.suppressions.create(tenantId: string, address: string, reason?: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/suppressions`\n\nAdd an email address to the tenant's suppression list. The address will\nnot receive any emails from this tenant until removed.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `address: string`\n Email address to suppress\n\n- `reason?: string`\n Reason for suppression (accepts null)\n\n### Returns\n\n- `{ data: { id: string; address: string; createdAt: string; reason?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; address: string; createdAt: string; reason?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst suppression = await client.tenants.suppressions.create('cm6abc123def456', { address: 'user@example.com' });\n\nconsole.log(suppression);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Suppressions.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tsuppression, err := client.Tenants.Suppressions.New(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantSuppressionNewParams{\n\t\t\tAddress: "user@example.com",\n\t\t\tReason: ark.String("user requested removal"),\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", suppression.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/suppressions \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "address": "user@example.com",\n "reason": "user requested removal"\n }\'', + }, + python: { + method: 'tenants.suppressions.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nsuppression = client.tenants.suppressions.create(\n tenant_id="cm6abc123def456",\n address="user@example.com",\n reason="user requested removal",\n)\nprint(suppression.data)', + }, + ruby: { + method: 'tenants.suppressions.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nsuppression = ark.tenants.suppressions.create("cm6abc123def456", address: "user@example.com")\n\nputs(suppression)', + }, + typescript: { + method: 'client.tenants.suppressions.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst suppression = await client.tenants.suppressions.create('cm6abc123def456', {\n address: 'user@example.com',\n reason: 'user requested removal',\n});\n\nconsole.log(suppression.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/tenants/{tenantId}/suppressions', + httpMethod: 'get', + summary: 'List suppressed addresses', + description: + "Get all email addresses on the tenant's suppression list. These addresses\nwill not receive any emails from this tenant.\n", + stainlessPath: '(resource) tenants.suppressions > (method) list', + qualified: 'client.tenants.suppressions.list', + params: ['tenantId: string;', 'page?: number;', 'perPage?: number;'], + response: '{ id: string; address: string; createdAt: string; reason?: string; }', + markdown: + "## list\n\n`client.tenants.suppressions.list(tenantId: string, page?: number, perPage?: number): { id: string; address: string; createdAt: string; reason?: string; }`\n\n**get** `/tenants/{tenantId}/suppressions`\n\nGet all email addresses on the tenant's suppression list. These addresses\nwill not receive any emails from this tenant.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `page?: number`\n\n- `perPage?: number`\n\n### Returns\n\n- `{ id: string; address: string; createdAt: string; reason?: string; }`\n\n - `id: string`\n - `address: string`\n - `createdAt: string`\n - `reason?: string`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const suppressionListResponse of client.tenants.suppressions.list('cm6abc123def456')) {\n console.log(suppressionListResponse);\n}\n```", + perLanguage: { + go: { + method: 'client.Tenants.Suppressions.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Tenants.Suppressions.List(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantSuppressionListParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/suppressions \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.suppressions.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.tenants.suppressions.list(\n tenant_id="cm6abc123def456",\n)\npage = page.data[0]\nprint(page.id)', + }, + ruby: { + method: 'tenants.suppressions.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.tenants.suppressions.list("cm6abc123def456")\n\nputs(page)', + }, + typescript: { + method: 'client.tenants.suppressions.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const suppressionListResponse of client.tenants.suppressions.list('cm6abc123def456')) {\n console.log(suppressionListResponse.id);\n}", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}/suppressions/{email}', + httpMethod: 'get', + summary: 'Check if address is suppressed', + description: "Check if a specific email address is on the tenant's suppression list.", + stainlessPath: '(resource) tenants.suppressions > (method) retrieve', + qualified: 'client.tenants.suppressions.retrieve', + params: ['tenantId: string;', 'email: string;'], + response: + '{ data: { address: string; suppressed: boolean; createdAt?: string; reason?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.tenants.suppressions.retrieve(tenantId: string, email: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/suppressions/{email}`\n\nCheck if a specific email address is on the tenant's suppression list.\n\n### Parameters\n\n- `tenantId: string`\n\n- `email: string`\n\n### Returns\n\n- `{ data: { address: string; suppressed: boolean; createdAt?: string; reason?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { address: string; suppressed: boolean; createdAt?: string; reason?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst suppression = await client.tenants.suppressions.retrieve('user@example.com', { tenantId: 'cm6abc123def456' });\n\nconsole.log(suppression);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Suppressions.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tsuppression, err := client.Tenants.Suppressions.Get(\n\t\tcontext.TODO(),\n\t\t"user@example.com",\n\t\tark.TenantSuppressionGetParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", suppression.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/suppressions/$EMAIL \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.suppressions.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nsuppression = client.tenants.suppressions.retrieve(\n email="user@example.com",\n tenant_id="cm6abc123def456",\n)\nprint(suppression.data)', + }, + ruby: { + method: 'tenants.suppressions.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nsuppression = ark.tenants.suppressions.retrieve("user@example.com", tenant_id: "cm6abc123def456")\n\nputs(suppression)', + }, + typescript: { + method: 'client.tenants.suppressions.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst suppression = await client.tenants.suppressions.retrieve('user@example.com', {\n tenantId: 'cm6abc123def456',\n});\n\nconsole.log(suppression.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/tenants/{tenantId}/suppressions/{email}', + httpMethod: 'delete', + summary: 'Remove from suppression list', + description: + "Remove an email address from the tenant's suppression list. The address\nwill be able to receive emails from this tenant again.\n", + stainlessPath: '(resource) tenants.suppressions > (method) delete', + qualified: 'client.tenants.suppressions.delete', + params: ['tenantId: string;', 'email: string;'], + response: '{ data: { message: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.tenants.suppressions.delete(tenantId: string, email: string): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/tenants/{tenantId}/suppressions/{email}`\n\nRemove an email address from the tenant's suppression list. The address\nwill be able to receive emails from this tenant again.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `email: string`\n\n### Returns\n\n- `{ data: { message: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { message: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst suppression = await client.tenants.suppressions.delete('user@example.com', { tenantId: 'cm6abc123def456' });\n\nconsole.log(suppression);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Suppressions.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tsuppression, err := client.Tenants.Suppressions.Delete(\n\t\tcontext.TODO(),\n\t\t"user@example.com",\n\t\tark.TenantSuppressionDeleteParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", suppression.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/suppressions/$EMAIL \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.suppressions.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nsuppression = client.tenants.suppressions.delete(\n email="user@example.com",\n tenant_id="cm6abc123def456",\n)\nprint(suppression.data)', + }, + ruby: { + method: 'tenants.suppressions.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nsuppression = ark.tenants.suppressions.delete("user@example.com", tenant_id: "cm6abc123def456")\n\nputs(suppression)', + }, + typescript: { + method: 'client.tenants.suppressions.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst suppression = await client.tenants.suppressions.delete('user@example.com', {\n tenantId: 'cm6abc123def456',\n});\n\nconsole.log(suppression.data);", + }, + }, + }, + { + name: 'create', + endpoint: '/tenants/{tenantId}/webhooks', + httpMethod: 'post', + summary: 'Create a webhook', + description: + 'Create a webhook endpoint to receive email event notifications for a tenant.\n\n**Available events:**\n- `MessageSent` - Email accepted by recipient server\n- `MessageDeliveryFailed` - Delivery permanently failed\n- `MessageDelayed` - Delivery temporarily failed, will retry\n- `MessageBounced` - Email bounced\n- `MessageHeld` - Email held for review\n- `MessageLinkClicked` - Recipient clicked a link\n- `MessageLoaded` - Recipient opened the email\n- `DomainDNSError` - Domain DNS issue detected\n', + stainlessPath: '(resource) tenants.webhooks > (method) create', + qualified: 'client.tenants.webhooks.create', + params: [ + 'tenantId: string;', + 'name: string;', + 'url: string;', + 'allEvents?: boolean;', + 'enabled?: boolean;', + 'events?: string[];', + ], + response: + '{ data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## create\n\n`client.tenants.webhooks.create(tenantId: string, name: string, url: string, allEvents?: boolean, enabled?: boolean, events?: string[]): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/webhooks`\n\nCreate a webhook endpoint to receive email event notifications for a tenant.\n\n**Available events:**\n- `MessageSent` - Email accepted by recipient server\n- `MessageDeliveryFailed` - Delivery permanently failed\n- `MessageDelayed` - Delivery temporarily failed, will retry\n- `MessageBounced` - Email bounced\n- `MessageHeld` - Email held for review\n- `MessageLinkClicked` - Recipient clicked a link\n- `MessageLoaded` - Recipient opened the email\n- `DomainDNSError` - Domain DNS issue detected\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `name: string`\n Webhook name for identification\n\n- `url: string`\n HTTPS endpoint URL\n\n- `allEvents?: boolean`\n Subscribe to all events (ignores events array, accepts null)\n\n- `enabled?: boolean`\n Whether the webhook is enabled (accepts null)\n\n- `events?: string[]`\n Events to subscribe to (accepts null):\n- `MessageSent` - Email successfully delivered to recipient's server\n- `MessageDelayed` - Temporary delivery failure, will retry\n- `MessageDeliveryFailed` - Permanent delivery failure\n- `MessageHeld` - Email held for manual review\n- `MessageBounced` - Email bounced back\n- `MessageLinkClicked` - Recipient clicked a tracked link\n- `MessageLoaded` - Recipient opened the email (tracking pixel loaded)\n- `DomainDNSError` - DNS configuration issue detected\n\n\n### Returns\n\n- `{ data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.tenants.webhooks.create('cm6abc123def456', { name: 'My App Webhook', url: 'https://myapp.com/webhooks/email' });\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Tenants.Webhooks.New(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantWebhookNewParams{\n\t\t\tName: "My App Webhook",\n\t\t\tURL: "https://myapp.com/webhooks/email",\n\t\t\tEvents: []string{"MessageSent", "MessageDeliveryFailed", "MessageBounced"},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "name": "My App Webhook",\n "url": "https://myapp.com/webhooks/email"\n }\'', + }, + python: { + method: 'tenants.webhooks.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.tenants.webhooks.create(\n tenant_id="cm6abc123def456",\n name="My App Webhook",\n url="https://myapp.com/webhooks/email",\n events=["MessageSent", "MessageDeliveryFailed", "MessageBounced"],\n)\nprint(webhook.data)', + }, + ruby: { + method: 'tenants.webhooks.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.tenants.webhooks.create(\n "cm6abc123def456",\n name: "My App Webhook",\n url: "https://myapp.com/webhooks/email"\n)\n\nputs(webhook)', + }, + typescript: { + method: 'client.tenants.webhooks.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.tenants.webhooks.create('cm6abc123def456', {\n name: 'My App Webhook',\n url: 'https://myapp.com/webhooks/email',\n events: ['MessageSent', 'MessageDeliveryFailed', 'MessageBounced'],\n});\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/tenants/{tenantId}/webhooks', + httpMethod: 'get', + summary: 'List webhooks', + description: 'Get all configured webhook endpoints for a tenant.', + stainlessPath: '(resource) tenants.webhooks > (method) list', + qualified: 'client.tenants.webhooks.list', + params: ['tenantId: string;'], + response: + '{ data: { webhooks: { id: string; enabled: boolean; events: string[]; name: string; url: string; }[]; }; meta: { requestId: string; }; success: true; }', + markdown: + "## list\n\n`client.tenants.webhooks.list(tenantId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/webhooks`\n\nGet all configured webhook endpoints for a tenant.\n\n### Parameters\n\n- `tenantId: string`\n\n### Returns\n\n- `{ data: { webhooks: { id: string; enabled: boolean; events: string[]; name: string; url: string; }[]; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { webhooks: { id: string; enabled: boolean; events: string[]; name: string; url: string; }[]; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhooks = await client.tenants.webhooks.list('cm6abc123def456');\n\nconsole.log(webhooks);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhooks, err := client.Tenants.Webhooks.List(context.TODO(), "cm6abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhooks.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.webhooks.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhooks = client.tenants.webhooks.list(\n "cm6abc123def456",\n)\nprint(webhooks.data)', + }, + ruby: { + method: 'tenants.webhooks.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhooks = ark.tenants.webhooks.list("cm6abc123def456")\n\nputs(webhooks)', + }, + typescript: { + method: 'client.tenants.webhooks.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhooks = await client.tenants.webhooks.list('cm6abc123def456');\n\nconsole.log(webhooks.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}', + httpMethod: 'get', + summary: 'Get webhook details', + description: 'Get webhook details', + stainlessPath: '(resource) tenants.webhooks > (method) retrieve', + qualified: 'client.tenants.webhooks.retrieve', + params: ['tenantId: string;', 'webhookId: string;'], + response: + '{ data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.tenants.webhooks.retrieve(tenantId: string, webhookId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/webhooks/{webhookId}`\n\nGet webhook details\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n### Returns\n\n- `{ data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.tenants.webhooks.retrieve('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Tenants.Webhooks.Get(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantWebhookGetParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.webhooks.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.tenants.webhooks.retrieve(\n webhook_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(webhook.data)', + }, + ruby: { + method: 'tenants.webhooks.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.tenants.webhooks.retrieve("123", tenant_id: "cm6abc123def456")\n\nputs(webhook)', + }, + typescript: { + method: 'client.tenants.webhooks.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.tenants.webhooks.retrieve('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'update', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}', + httpMethod: 'patch', + summary: 'Update a webhook', + description: 'Update a webhook', + stainlessPath: '(resource) tenants.webhooks > (method) update', + qualified: 'client.tenants.webhooks.update', + params: [ + 'tenantId: string;', + 'webhookId: string;', + 'allEvents?: boolean;', + 'enabled?: boolean;', + 'events?: string[];', + 'name?: string;', + 'url?: string;', + ], + response: + '{ data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## update\n\n`client.tenants.webhooks.update(tenantId: string, webhookId: string, allEvents?: boolean, enabled?: boolean, events?: string[], name?: string, url?: string): { data: object; meta: api_meta; success: true; }`\n\n**patch** `/tenants/{tenantId}/webhooks/{webhookId}`\n\nUpdate a webhook\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n- `allEvents?: boolean`\n\n- `enabled?: boolean`\n\n- `events?: string[]`\n\n- `name?: string`\n\n- `url?: string`\n\n### Returns\n\n- `{ data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; allEvents: boolean; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; uuid: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.tenants.webhooks.update('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.Update', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Tenants.Webhooks.Update(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantWebhookUpdateParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + "curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID \\\n -X PATCH \\\n -H 'Content-Type: application/json' \\\n -H \"Authorization: Bearer $ARK_API_KEY\" \\\n -d '{}'", + }, + python: { + method: 'tenants.webhooks.update', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.tenants.webhooks.update(\n webhook_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(webhook.data)', + }, + ruby: { + method: 'tenants.webhooks.update', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.tenants.webhooks.update("123", tenant_id: "cm6abc123def456")\n\nputs(webhook)', + }, + typescript: { + method: 'client.tenants.webhooks.update', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.tenants.webhooks.update('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}', + httpMethod: 'delete', + summary: 'Delete a webhook', + description: 'Delete a webhook', + stainlessPath: '(resource) tenants.webhooks > (method) delete', + qualified: 'client.tenants.webhooks.delete', + params: ['tenantId: string;', 'webhookId: string;'], + response: '{ data: { message: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.tenants.webhooks.delete(tenantId: string, webhookId: string): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/tenants/{tenantId}/webhooks/{webhookId}`\n\nDelete a webhook\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n### Returns\n\n- `{ data: { message: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { message: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.tenants.webhooks.delete('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Tenants.Webhooks.Delete(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantWebhookDeleteParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.webhooks.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.tenants.webhooks.delete(\n webhook_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(webhook.data)', + }, + ruby: { + method: 'tenants.webhooks.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.tenants.webhooks.delete("123", tenant_id: "cm6abc123def456")\n\nputs(webhook)', + }, + typescript: { + method: 'client.tenants.webhooks.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.tenants.webhooks.delete('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'test', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}/test', + httpMethod: 'post', + summary: 'Test a webhook', + description: + 'Send a test payload to your webhook endpoint and verify it receives the data correctly.\n\nUse this to:\n- Verify your webhook URL is accessible\n- Test your signature verification code\n- Ensure your server handles the payload format correctly\n\n**Test payload format:**\nThe test payload is identical to real webhook payloads, containing sample data\nfor the specified event type. Your webhook should respond with a 2xx status code.\n', + stainlessPath: '(resource) tenants.webhooks > (method) test', + qualified: 'client.tenants.webhooks.test', + params: ['tenantId: string;', 'webhookId: string;', 'event: string;'], + response: + '{ data: { duration: number; event: string; statusCode: number; success: boolean; body?: string; error?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## test\n\n`client.tenants.webhooks.test(tenantId: string, webhookId: string, event: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/webhooks/{webhookId}/test`\n\nSend a test payload to your webhook endpoint and verify it receives the data correctly.\n\nUse this to:\n- Verify your webhook URL is accessible\n- Test your signature verification code\n- Ensure your server handles the payload format correctly\n\n**Test payload format:**\nThe test payload is identical to real webhook payloads, containing sample data\nfor the specified event type. Your webhook should respond with a 2xx status code.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n- `event: string`\n Event type to simulate\n\n### Returns\n\n- `{ data: { duration: number; event: string; statusCode: number; success: boolean; body?: string; error?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { duration: number; event: string; statusCode: number; success: boolean; body?: string; error?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.webhooks.test('123', { tenantId: 'cm6abc123def456', event: 'MessageSent' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.Test', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Webhooks.Test(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantWebhookTestParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t\tEvent: ark.TenantWebhookTestParamsEventMessageSent,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID/test \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "event": "MessageSent"\n }\'', + }, + python: { + method: 'tenants.webhooks.test', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.webhooks.test(\n webhook_id="123",\n tenant_id="cm6abc123def456",\n event="MessageSent",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.webhooks.test_', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.webhooks.test_("123", tenant_id: "cm6abc123def456", event: :MessageSent)\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.webhooks.test', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.webhooks.test('123', {\n tenantId: 'cm6abc123def456',\n event: 'MessageSent',\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'list_deliveries', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}/deliveries', + httpMethod: 'get', + summary: 'List webhook deliveries', + description: + 'Get a paginated list of delivery attempts for a specific webhook.\n\nUse this to:\n- Monitor webhook health and delivery success rate\n- Debug failed deliveries\n- Find specific events to replay\n\n**Filtering:**\n- Filter by success/failure to find problematic deliveries\n- Filter by event type to find specific events\n- Filter by time range for debugging recent issues\n\n**Retry behavior:**\nFailed deliveries are automatically retried with exponential backoff over ~3 days.\nCheck `willRetry` to see if more attempts are scheduled.\n', + stainlessPath: '(resource) tenants.webhooks > (method) list_deliveries', + qualified: 'client.tenants.webhooks.listDeliveries', + params: [ + 'tenantId: string;', + 'webhookId: string;', + 'after?: number;', + 'before?: number;', + 'event?: string;', + 'page?: number;', + 'perPage?: number;', + 'success?: boolean;', + ], + response: + '{ data: { id: string; attempt: number; event: string; statusCode: number; success: boolean; timestamp: string; url: string; webhookId: string; willRetry: boolean; }[]; meta: { requestId: string; }; page: number; perPage: number; total: number; totalPages: number; }', + markdown: + "## list_deliveries\n\n`client.tenants.webhooks.listDeliveries(tenantId: string, webhookId: string, after?: number, before?: number, event?: string, page?: number, perPage?: number, success?: boolean): { data: object[]; meta: api_meta; page: number; perPage: number; total: number; totalPages: number; }`\n\n**get** `/tenants/{tenantId}/webhooks/{webhookId}/deliveries`\n\nGet a paginated list of delivery attempts for a specific webhook.\n\nUse this to:\n- Monitor webhook health and delivery success rate\n- Debug failed deliveries\n- Find specific events to replay\n\n**Filtering:**\n- Filter by success/failure to find problematic deliveries\n- Filter by event type to find specific events\n- Filter by time range for debugging recent issues\n\n**Retry behavior:**\nFailed deliveries are automatically retried with exponential backoff over ~3 days.\nCheck `willRetry` to see if more attempts are scheduled.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n- `after?: number`\n Only deliveries after this Unix timestamp\n\n- `before?: number`\n Only deliveries before this Unix timestamp\n\n- `event?: string`\n Filter by event type\n\n- `page?: number`\n Page number (default 1)\n\n- `perPage?: number`\n Items per page (default 30, max 100)\n\n- `success?: boolean`\n Filter by delivery success (true = 2xx response, false = non-2xx or error)\n\n### Returns\n\n- `{ data: { id: string; attempt: number; event: string; statusCode: number; success: boolean; timestamp: string; url: string; webhookId: string; willRetry: boolean; }[]; meta: { requestId: string; }; page: number; perPage: number; total: number; totalPages: number; }`\n Paginated list of webhook delivery attempts\n\n - `data: { id: string; attempt: number; event: string; statusCode: number; success: boolean; timestamp: string; url: string; webhookId: string; willRetry: boolean; }[]`\n - `meta: { requestId: string; }`\n - `page: number`\n - `perPage: number`\n - `total: number`\n - `totalPages: number`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.webhooks.listDeliveries('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.ListDeliveries', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Webhooks.ListDeliveries(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantWebhookListDeliveriesParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID/deliveries \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.webhooks.list_deliveries', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.webhooks.list_deliveries(\n webhook_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.webhooks.list_deliveries', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.webhooks.list_deliveries("123", tenant_id: "cm6abc123def456")\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.webhooks.listDeliveries', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.webhooks.listDeliveries('123', {\n tenantId: 'cm6abc123def456',\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'retrieve_delivery', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{deliveryId}', + httpMethod: 'get', + summary: 'Get webhook delivery details', + description: + 'Get detailed information about a specific webhook delivery attempt.\n\nReturns:\n- The complete request payload that was sent\n- Request headers including the signature\n- Response status code and body from your endpoint\n- Timing information\n\nUse this to debug why a delivery failed or verify what data was sent.\n', + stainlessPath: '(resource) tenants.webhooks > (method) retrieve_delivery', + qualified: 'client.tenants.webhooks.retrieveDelivery', + params: ['tenantId: string;', 'webhookId: string;', 'deliveryId: string;'], + response: + '{ data: { id: string; attempt: number; event: string; request: { headers: object; payload: object; }; response: { statusCode: number; body?: string; }; statusCode: number; success: boolean; timestamp: string; url: string; webhookId: string; webhookName: string; willRetry: boolean; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve_delivery\n\n`client.tenants.webhooks.retrieveDelivery(tenantId: string, webhookId: string, deliveryId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{deliveryId}`\n\nGet detailed information about a specific webhook delivery attempt.\n\nReturns:\n- The complete request payload that was sent\n- Request headers including the signature\n- Response status code and body from your endpoint\n- Timing information\n\nUse this to debug why a delivery failed or verify what data was sent.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n- `deliveryId: string`\n\n### Returns\n\n- `{ data: { id: string; attempt: number; event: string; request: { headers: object; payload: object; }; response: { statusCode: number; body?: string; }; statusCode: number; success: boolean; timestamp: string; url: string; webhookId: string; webhookName: string; willRetry: boolean; }; meta: { requestId: string; }; success: true; }`\n Detailed information about a webhook delivery attempt\n\n - `data: { id: string; attempt: number; event: string; request: { headers: object; payload: object; }; response: { statusCode: number; body?: string; }; statusCode: number; success: boolean; timestamp: string; url: string; webhookId: string; webhookName: string; willRetry: boolean; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.webhooks.retrieveDelivery('whr_abc123def456', { tenantId: 'cm6abc123def456', webhookId: '123' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.GetDelivery', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Webhooks.GetDelivery(\n\t\tcontext.TODO(),\n\t\t"whr_abc123def456",\n\t\tark.TenantWebhookGetDeliveryParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t\tWebhookID: "123",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID/deliveries/$DELIVERY_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.webhooks.retrieve_delivery', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.webhooks.retrieve_delivery(\n delivery_id="whr_abc123def456",\n tenant_id="cm6abc123def456",\n webhook_id="123",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.webhooks.retrieve_delivery', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.webhooks.retrieve_delivery(\n "whr_abc123def456",\n tenant_id: "cm6abc123def456",\n webhook_id: "123"\n)\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.webhooks.retrieveDelivery', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.webhooks.retrieveDelivery('whr_abc123def456', {\n tenantId: 'cm6abc123def456',\n webhookId: '123',\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'replay_delivery', + endpoint: '/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{deliveryId}/replay', + httpMethod: 'post', + summary: 'Replay a webhook delivery', + description: + 'Re-send a webhook delivery to your endpoint.\n\n**Use cases:**\n- Recover from transient failures after fixing your endpoint\n- Test endpoint changes with real historical data\n- Retry deliveries that failed due to downtime\n\n**How it works:**\n1. Fetches the original payload from the delivery\n2. Generates a new timestamp and signature\n3. Sends to your webhook URL immediately\n4. Returns the result (does not queue for retry if it fails)\n\n**Note:** The webhook must be enabled to replay deliveries.\n', + stainlessPath: '(resource) tenants.webhooks > (method) replay_delivery', + qualified: 'client.tenants.webhooks.replayDelivery', + params: ['tenantId: string;', 'webhookId: string;', 'deliveryId: string;'], + response: + '{ data: { duration: number; newDeliveryId: string; originalDeliveryId: string; statusCode: number; success: boolean; timestamp: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## replay_delivery\n\n`client.tenants.webhooks.replayDelivery(tenantId: string, webhookId: string, deliveryId: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{deliveryId}/replay`\n\nRe-send a webhook delivery to your endpoint.\n\n**Use cases:**\n- Recover from transient failures after fixing your endpoint\n- Test endpoint changes with real historical data\n- Retry deliveries that failed due to downtime\n\n**How it works:**\n1. Fetches the original payload from the delivery\n2. Generates a new timestamp and signature\n3. Sends to your webhook URL immediately\n4. Returns the result (does not queue for retry if it fails)\n\n**Note:** The webhook must be enabled to replay deliveries.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `webhookId: string`\n\n- `deliveryId: string`\n\n### Returns\n\n- `{ data: { duration: number; newDeliveryId: string; originalDeliveryId: string; statusCode: number; success: boolean; timestamp: string; }; meta: { requestId: string; }; success: true; }`\n Result of replaying a webhook delivery\n\n - `data: { duration: number; newDeliveryId: string; originalDeliveryId: string; statusCode: number; success: boolean; timestamp: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.webhooks.replayDelivery('whr_abc123def456', { tenantId: 'cm6abc123def456', webhookId: '123' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Webhooks.ReplayDelivery', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Webhooks.ReplayDelivery(\n\t\tcontext.TODO(),\n\t\t"whr_abc123def456",\n\t\tark.TenantWebhookReplayDeliveryParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t\tWebhookID: "123",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/webhooks/$WEBHOOK_ID/deliveries/$DELIVERY_ID/replay \\\n -X POST \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.webhooks.replay_delivery', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.webhooks.replay_delivery(\n delivery_id="whr_abc123def456",\n tenant_id="cm6abc123def456",\n webhook_id="123",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.webhooks.replay_delivery', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.webhooks.replay_delivery("whr_abc123def456", tenant_id: "cm6abc123def456", webhook_id: "123")\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.webhooks.replayDelivery', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.webhooks.replayDelivery('whr_abc123def456', {\n tenantId: 'cm6abc123def456',\n webhookId: '123',\n});\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'create', + endpoint: '/tenants/{tenantId}/tracking', + httpMethod: 'post', + summary: 'Create track domain', + description: + 'Create a new track domain for open/click tracking for a tenant.\n\nAfter creation, you must configure a CNAME record pointing to\nthe provided DNS value before tracking will work.\n', + stainlessPath: '(resource) tenants.tracking > (method) create', + qualified: 'client.tenants.tracking.create', + params: [ + 'tenantId: string;', + 'domainId: number;', + 'name: string;', + 'sslEnabled?: boolean;', + 'trackClicks?: boolean;', + 'trackOpens?: boolean;', + ], + response: + "{ data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: object; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## create\n\n`client.tenants.tracking.create(tenantId: string, domainId: number, name: string, sslEnabled?: boolean, trackClicks?: boolean, trackOpens?: boolean): { data: track_domain; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/tracking`\n\nCreate a new track domain for open/click tracking for a tenant.\n\nAfter creation, you must configure a CNAME record pointing to\nthe provided DNS value before tracking will work.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `domainId: number`\n ID of the sending domain to attach this track domain to\n\n- `name: string`\n Subdomain name (e.g., 'track' for track.yourdomain.com)\n\n- `sslEnabled?: boolean`\n Enable SSL for tracking URLs (accepts null, defaults to true)\n\n- `trackClicks?: boolean`\n Enable click tracking (accepts null, defaults to true)\n\n- `trackOpens?: boolean`\n Enable open tracking (tracking pixel, accepts null, defaults to true)\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: object; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tracking = await client.tenants.tracking.create('cm6abc123def456', { domainId: 123, name: 'track' });\n\nconsole.log(tracking);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Tracking.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttracking, err := client.Tenants.Tracking.New(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantTrackingNewParams{\n\t\t\tDomainID: 123,\n\t\t\tName: "track",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tracking.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/tracking \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "domainId": 123,\n "name": "track"\n }\'', + }, + python: { + method: 'tenants.tracking.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntracking = client.tenants.tracking.create(\n tenant_id="cm6abc123def456",\n domain_id=123,\n name="track",\n)\nprint(tracking.data)', + }, + ruby: { + method: 'tenants.tracking.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntracking = ark.tenants.tracking.create("cm6abc123def456", domain_id: 123, name: "track")\n\nputs(tracking)', + }, + typescript: { + method: 'client.tenants.tracking.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tracking = await client.tenants.tracking.create('cm6abc123def456', {\n domainId: 123,\n name: 'track',\n});\n\nconsole.log(tracking.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/tenants/{tenantId}/tracking', + httpMethod: 'get', + summary: 'List track domains', + description: + 'List all track domains configured for a tenant.\nTrack domains enable open and click tracking for emails.\n', + stainlessPath: '(resource) tenants.tracking > (method) list', + qualified: 'client.tenants.tracking.list', + params: ['tenantId: string;'], + response: '{ data: { trackDomains: object[]; }; meta: { requestId: string; }; success: true; }', + markdown: + "## list\n\n`client.tenants.tracking.list(tenantId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/tracking`\n\nList all track domains configured for a tenant.\nTrack domains enable open and click tracking for emails.\n\n\n### Parameters\n\n- `tenantId: string`\n\n### Returns\n\n- `{ data: { trackDomains: object[]; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { trackDomains: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }[]; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst trackings = await client.tenants.tracking.list('cm6abc123def456');\n\nconsole.log(trackings);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Tracking.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttrackings, err := client.Tenants.Tracking.List(context.TODO(), "cm6abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", trackings.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/tracking \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.tracking.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntrackings = client.tenants.tracking.list(\n "cm6abc123def456",\n)\nprint(trackings.data)', + }, + ruby: { + method: 'tenants.tracking.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntrackings = ark.tenants.tracking.list("cm6abc123def456")\n\nputs(trackings)', + }, + typescript: { + method: 'client.tenants.tracking.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst trackings = await client.tenants.tracking.list('cm6abc123def456');\n\nconsole.log(trackings.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}/tracking/{trackingId}', + httpMethod: 'get', + summary: 'Get track domain', + description: 'Get details of a specific track domain including DNS configuration.', + stainlessPath: '(resource) tenants.tracking > (method) retrieve', + qualified: 'client.tenants.tracking.retrieve', + params: ['tenantId: string;', 'trackingId: string;'], + response: + "{ data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: object; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## retrieve\n\n`client.tenants.tracking.retrieve(tenantId: string, trackingId: string): { data: track_domain; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/tracking/{trackingId}`\n\nGet details of a specific track domain including DNS configuration.\n\n### Parameters\n\n- `tenantId: string`\n\n- `trackingId: string`\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: object; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tracking = await client.tenants.tracking.retrieve('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(tracking);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Tracking.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttracking, err := client.Tenants.Tracking.Get(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantTrackingGetParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tracking.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/tracking/$TRACKING_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.tracking.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntracking = client.tenants.tracking.retrieve(\n tracking_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(tracking.data)', + }, + ruby: { + method: 'tenants.tracking.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntracking = ark.tenants.tracking.retrieve("123", tenant_id: "cm6abc123def456")\n\nputs(tracking)', + }, + typescript: { + method: 'client.tenants.tracking.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tracking = await client.tenants.tracking.retrieve('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(tracking.data);", + }, + }, + }, + { + name: 'update', + endpoint: '/tenants/{tenantId}/tracking/{trackingId}', + httpMethod: 'patch', + summary: 'Update track domain', + description: + 'Update track domain settings.\n\nUse this to:\n- Enable/disable click tracking\n- Enable/disable open tracking\n- Enable/disable SSL\n- Set excluded click domains\n', + stainlessPath: '(resource) tenants.tracking > (method) update', + qualified: 'client.tenants.tracking.update', + params: [ + 'tenantId: string;', + 'trackingId: string;', + 'excludedClickDomains?: string;', + 'sslEnabled?: boolean;', + 'trackClicks?: boolean;', + 'trackOpens?: boolean;', + ], + response: + "{ data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: object; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## update\n\n`client.tenants.tracking.update(tenantId: string, trackingId: string, excludedClickDomains?: string, sslEnabled?: boolean, trackClicks?: boolean, trackOpens?: boolean): { data: track_domain; meta: api_meta; success: true; }`\n\n**patch** `/tenants/{tenantId}/tracking/{trackingId}`\n\nUpdate track domain settings.\n\nUse this to:\n- Enable/disable click tracking\n- Enable/disable open tracking\n- Enable/disable SSL\n- Set excluded click domains\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `trackingId: string`\n\n- `excludedClickDomains?: string`\n Comma-separated list of domains to exclude from click tracking (accepts null)\n\n- `sslEnabled?: boolean`\n Enable or disable SSL for tracking URLs (accepts null)\n\n- `trackClicks?: boolean`\n Enable or disable click tracking (accepts null)\n\n- `trackOpens?: boolean`\n Enable or disable open tracking (accepts null)\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: object; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; dnsOk: boolean; domainId: string; fullName: string; name: string; sslEnabled: boolean; trackClicks: boolean; trackOpens: boolean; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; dnsStatus?: 'ok' | 'missing' | 'invalid'; excludedClickDomains?: string; updatedAt?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tracking = await client.tenants.tracking.update('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(tracking);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Tracking.Update', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttracking, err := client.Tenants.Tracking.Update(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantTrackingUpdateParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tracking.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/tracking/$TRACKING_ID \\\n -X PATCH \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "excludedClickDomains": "example.com,mysite.org"\n }\'', + }, + python: { + method: 'tenants.tracking.update', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntracking = client.tenants.tracking.update(\n tracking_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(tracking.data)', + }, + ruby: { + method: 'tenants.tracking.update', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntracking = ark.tenants.tracking.update("123", tenant_id: "cm6abc123def456")\n\nputs(tracking)', + }, + typescript: { + method: 'client.tenants.tracking.update', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tracking = await client.tenants.tracking.update('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(tracking.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/tenants/{tenantId}/tracking/{trackingId}', + httpMethod: 'delete', + summary: 'Delete track domain', + description: 'Delete a track domain. This will disable tracking for any emails using this domain.', + stainlessPath: '(resource) tenants.tracking > (method) delete', + qualified: 'client.tenants.tracking.delete', + params: ['tenantId: string;', 'trackingId: string;'], + response: '{ data: { message: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.tenants.tracking.delete(tenantId: string, trackingId: string): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/tenants/{tenantId}/tracking/{trackingId}`\n\nDelete a track domain. This will disable tracking for any emails using this domain.\n\n### Parameters\n\n- `tenantId: string`\n\n- `trackingId: string`\n\n### Returns\n\n- `{ data: { message: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { message: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst tracking = await client.tenants.tracking.delete('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(tracking);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Tracking.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\ttracking, err := client.Tenants.Tracking.Delete(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantTrackingDeleteParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", tracking.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/tracking/$TRACKING_ID \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.tracking.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\ntracking = client.tenants.tracking.delete(\n tracking_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(tracking.data)', + }, + ruby: { + method: 'tenants.tracking.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\ntracking = ark.tenants.tracking.delete("123", tenant_id: "cm6abc123def456")\n\nputs(tracking)', + }, + typescript: { + method: 'client.tenants.tracking.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst tracking = await client.tenants.tracking.delete('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(tracking.data);", + }, + }, + }, + { + name: 'verify', + endpoint: '/tenants/{tenantId}/tracking/{trackingId}/verify', + httpMethod: 'post', + summary: 'Verify track domain DNS', + description: + 'Check DNS configuration for the track domain.\n\nThe track domain requires a CNAME record to be configured before\nopen and click tracking will work. Use this endpoint to verify\nthe DNS is correctly set up.\n', + stainlessPath: '(resource) tenants.tracking > (method) verify', + qualified: 'client.tenants.tracking.verify', + params: ['tenantId: string;', 'trackingId: string;'], + response: + "{ data: { id: string; dnsOk: boolean; dnsStatus: 'ok' | 'missing' | 'invalid'; fullName: string; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; }; meta: { requestId: string; }; success: true; }", + markdown: + "## verify\n\n`client.tenants.tracking.verify(tenantId: string, trackingId: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/tenants/{tenantId}/tracking/{trackingId}/verify`\n\nCheck DNS configuration for the track domain.\n\nThe track domain requires a CNAME record to be configured before\nopen and click tracking will work. Use this endpoint to verify\nthe DNS is correctly set up.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `trackingId: string`\n\n### Returns\n\n- `{ data: { id: string; dnsOk: boolean; dnsStatus: 'ok' | 'missing' | 'invalid'; fullName: string; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; dnsOk: boolean; dnsStatus: 'ok' | 'missing' | 'invalid'; fullName: string; dnsCheckedAt?: string; dnsError?: string; dnsRecord?: { name?: string; type?: string; value?: string; }; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.tracking.verify('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Tracking.Verify', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Tracking.Verify(\n\t\tcontext.TODO(),\n\t\t"123",\n\t\tark.TenantTrackingVerifyParams{\n\t\t\tTenantID: "cm6abc123def456",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/tracking/$TRACKING_ID/verify \\\n -X POST \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.tracking.verify', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.tracking.verify(\n tracking_id="123",\n tenant_id="cm6abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.tracking.verify', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.tracking.verify("123", tenant_id: "cm6abc123def456")\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.tracking.verify', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.tracking.verify('123', { tenantId: 'cm6abc123def456' });\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/tenants/{tenantId}/usage', + httpMethod: 'get', + summary: 'Get usage stats for a tenant', + description: + 'Returns email sending statistics for a specific tenant over a time period.\n\n**Use cases:**\n- Display usage dashboard to your customers\n- Calculate per-tenant billing\n- Monitor tenant health and delivery rates\n\n**Period formats:**\n- Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_90_days`\n- Month: `2024-01` (full month)\n- Date range: `2024-01-01..2024-01-31`\n- Single day: `2024-01-15`\n\n**Response includes:**\n- `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held\n- `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)\n', + stainlessPath: '(resource) tenants.usage > (method) retrieve', + qualified: 'client.tenants.usage.retrieve', + params: ['tenantId: string;', 'period?: string;', 'timezone?: string;'], + response: + '{ data: { emails: email_counts; period: usage_period; rates: email_rates; tenant_id: string; tenant_name: string; external_id?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.tenants.usage.retrieve(tenantId: string, period?: string, timezone?: string): { data: tenant_usage; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/usage`\n\nReturns email sending statistics for a specific tenant over a time period.\n\n**Use cases:**\n- Display usage dashboard to your customers\n- Calculate per-tenant billing\n- Monitor tenant health and delivery rates\n\n**Period formats:**\n- Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_90_days`\n- Month: `2024-01` (full month)\n- Date range: `2024-01-01..2024-01-31`\n- Single day: `2024-01-15`\n\n**Response includes:**\n- `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held\n- `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `period?: string`\n Time period for usage data. Defaults to current month.\n\n**Formats:**\n- Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, `last_7_days`, `last_30_days`, `last_90_days`\n- Month: `2024-01`\n- Range: `2024-01-01..2024-01-31`\n- Day: `2024-01-15`\n\n\n- `timezone?: string`\n Timezone for period calculations (IANA format). Defaults to UTC.\n\n### Returns\n\n- `{ data: { emails: email_counts; period: usage_period; rates: email_rates; tenant_id: string; tenant_name: string; external_id?: string; }; meta: { requestId: string; }; success: true; }`\n Usage statistics for a single tenant\n\n - `data: { emails: { bounced: number; delivered: number; hard_failed: number; held: number; sent: number; soft_failed: number; }; period: { end: string; start: string; }; rates: { bounce_rate: number; delivery_rate: number; }; tenant_id: string; tenant_name: string; external_id?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst usage = await client.tenants.usage.retrieve('cm6abc123def456');\n\nconsole.log(usage);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Usage.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tusage, err := client.Tenants.Usage.Get(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantUsageGetParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", usage.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/usage \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.usage.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nusage = client.tenants.usage.retrieve(\n tenant_id="cm6abc123def456",\n)\nprint(usage.data)', + }, + ruby: { + method: 'tenants.usage.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nusage = ark.tenants.usage.retrieve("cm6abc123def456")\n\nputs(usage)', + }, + typescript: { + method: 'client.tenants.usage.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst usage = await client.tenants.usage.retrieve('cm6abc123def456');\n\nconsole.log(usage.data);", + }, + }, + }, + { + name: 'retrieve_timeseries', + endpoint: '/tenants/{tenantId}/usage/timeseries', + httpMethod: 'get', + summary: 'Get usage timeseries for a tenant', + description: + 'Returns time-bucketed email statistics for a specific tenant.\n\n**Use cases:**\n- Build usage charts and graphs\n- Identify sending patterns\n- Detect anomalies in delivery rates\n\n**Granularity options:**\n- `hour` - Hourly buckets (best for last 7 days)\n- `day` - Daily buckets (best for last 30-90 days)\n- `week` - Weekly buckets (best for last 6 months)\n- `month` - Monthly buckets (best for year-over-year)\n\nThe response includes a data point for each time bucket with all email metrics.\n', + stainlessPath: '(resource) tenants.usage > (method) retrieve_timeseries', + qualified: 'client.tenants.usage.retrieveTimeseries', + params: [ + 'tenantId: string;', + "granularity?: 'hour' | 'day' | 'week' | 'month';", + 'period?: string;', + 'timezone?: string;', + ], + response: + "{ data: { data: object[]; granularity: 'hour' | 'day' | 'week' | 'month'; period: usage_period; tenant_id: string; tenant_name: string; }; meta: { requestId: string; }; success: true; }", + markdown: + "## retrieve_timeseries\n\n`client.tenants.usage.retrieveTimeseries(tenantId: string, granularity?: 'hour' | 'day' | 'week' | 'month', period?: string, timezone?: string): { data: tenant_usage_timeseries; meta: api_meta; success: true; }`\n\n**get** `/tenants/{tenantId}/usage/timeseries`\n\nReturns time-bucketed email statistics for a specific tenant.\n\n**Use cases:**\n- Build usage charts and graphs\n- Identify sending patterns\n- Detect anomalies in delivery rates\n\n**Granularity options:**\n- `hour` - Hourly buckets (best for last 7 days)\n- `day` - Daily buckets (best for last 30-90 days)\n- `week` - Weekly buckets (best for last 6 months)\n- `month` - Monthly buckets (best for year-over-year)\n\nThe response includes a data point for each time bucket with all email metrics.\n\n\n### Parameters\n\n- `tenantId: string`\n\n- `granularity?: 'hour' | 'day' | 'week' | 'month'`\n Time bucket size for data points\n\n- `period?: string`\n Time period for timeseries data. Defaults to current month.\n\n- `timezone?: string`\n Timezone for period calculations (IANA format). Defaults to UTC.\n\n### Returns\n\n- `{ data: { data: object[]; granularity: 'hour' | 'day' | 'week' | 'month'; period: usage_period; tenant_id: string; tenant_name: string; }; meta: { requestId: string; }; success: true; }`\n Timeseries usage data for a tenant\n\n - `data: { data: { bounced: number; delivered: number; hard_failed: number; held: number; sent: number; soft_failed: number; timestamp: string; }[]; granularity: 'hour' | 'day' | 'week' | 'month'; period: { end: string; start: string; }; tenant_id: string; tenant_name: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.tenants.usage.retrieveTimeseries('cm6abc123def456');\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Tenants.Usage.GetTimeseries', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Tenants.Usage.GetTimeseries(\n\t\tcontext.TODO(),\n\t\t"cm6abc123def456",\n\t\tark.TenantUsageGetTimeseriesParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/tenants/$TENANT_ID/usage/timeseries \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'tenants.usage.retrieve_timeseries', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.tenants.usage.retrieve_timeseries(\n tenant_id="cm6abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'tenants.usage.retrieve_timeseries', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.tenants.usage.retrieve_timeseries("cm6abc123def456")\n\nputs(response)', + }, + typescript: { + method: 'client.tenants.usage.retrieveTimeseries', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.usage.retrieveTimeseries('cm6abc123def456');\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'create', + endpoint: '/platform/webhooks', + httpMethod: 'post', + summary: 'Create a platform webhook', + description: + 'Create a platform webhook to receive email event notifications from all tenants.\n\nPlatform webhooks receive events from **all tenants** in your organization.\nEach webhook payload includes a `tenant_id` field to identify which tenant\nthe event belongs to.\n\n**Available events:**\n- `MessageSent` - Email accepted by recipient server\n- `MessageDeliveryFailed` - Delivery permanently failed\n- `MessageDelayed` - Delivery temporarily failed, will retry\n- `MessageBounced` - Email bounced\n- `MessageHeld` - Email held for review\n- `MessageLinkClicked` - Recipient clicked a link\n- `MessageLoaded` - Recipient opened the email\n- `DomainDNSError` - Domain DNS issue detected\n\n**Webhook payload includes:**\n- `event` - The event type\n- `tenant_id` - The tenant that sent the email\n- `timestamp` - Unix timestamp of the event\n- `payload` - Event-specific data (message details, status, etc.)\n', + stainlessPath: '(resource) platform.webhooks > (method) create', + qualified: 'client.platform.webhooks.create', + params: ['name: string;', 'url: string;', 'events?: string[];'], + response: + '{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## create\n\n`client.platform.webhooks.create(name: string, url: string, events?: string[]): { data: object; meta: api_meta; success: true; }`\n\n**post** `/platform/webhooks`\n\nCreate a platform webhook to receive email event notifications from all tenants.\n\nPlatform webhooks receive events from **all tenants** in your organization.\nEach webhook payload includes a `tenant_id` field to identify which tenant\nthe event belongs to.\n\n**Available events:**\n- `MessageSent` - Email accepted by recipient server\n- `MessageDeliveryFailed` - Delivery permanently failed\n- `MessageDelayed` - Delivery temporarily failed, will retry\n- `MessageBounced` - Email bounced\n- `MessageHeld` - Email held for review\n- `MessageLinkClicked` - Recipient clicked a link\n- `MessageLoaded` - Recipient opened the email\n- `DomainDNSError` - Domain DNS issue detected\n\n**Webhook payload includes:**\n- `event` - The event type\n- `tenant_id` - The tenant that sent the email\n- `timestamp` - Unix timestamp of the event\n- `payload` - Event-specific data (message details, status, etc.)\n\n\n### Parameters\n\n- `name: string`\n Display name for the webhook\n\n- `url: string`\n Webhook endpoint URL (must be HTTPS)\n\n- `events?: string[]`\n Events to subscribe to. Empty array means all events.\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.platform.webhooks.create({ name: 'Central Event Processor', url: 'https://myplatform.com/webhooks/email-events' });\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Platform.Webhooks.New(context.TODO(), ark.PlatformWebhookNewParams{\n\t\tName: "Central Event Processor",\n\t\tURL: "https://myplatform.com/webhooks/email-events",\n\t\tEvents: []string{"MessageSent", "MessageDeliveryFailed", "MessageBounced"},\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "name": "Central Event Processor",\n "url": "https://myplatform.com/webhooks/email-events",\n "events": [\n "MessageSent",\n "MessageDeliveryFailed",\n "MessageBounced"\n ]\n }\'', + }, + python: { + method: 'platform.webhooks.create', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.platform.webhooks.create(\n name="Central Event Processor",\n url="https://myplatform.com/webhooks/email-events",\n events=["MessageSent", "MessageDeliveryFailed", "MessageBounced"],\n)\nprint(webhook.data)', + }, + ruby: { + method: 'platform.webhooks.create', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.platform.webhooks.create(\n name: "Central Event Processor",\n url: "https://myplatform.com/webhooks/email-events"\n)\n\nputs(webhook)', + }, + typescript: { + method: 'client.platform.webhooks.create', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.platform.webhooks.create({\n name: 'Central Event Processor',\n url: 'https://myplatform.com/webhooks/email-events',\n events: ['MessageSent', 'MessageDeliveryFailed', 'MessageBounced'],\n});\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'list', + endpoint: '/platform/webhooks', + httpMethod: 'get', + summary: 'List platform webhooks', + description: + 'Get all platform webhook endpoints configured for your organization.\n\nPlatform webhooks receive events from **all tenants** in your organization,\nunlike tenant webhooks which only receive events for a specific tenant.\nThis is useful for centralized event processing and monitoring.\n', + stainlessPath: '(resource) platform.webhooks > (method) list', + qualified: 'client.platform.webhooks.list', + response: + '{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; }[]; meta: { requestId: string; }; success: true; }', + markdown: + "## list\n\n`client.platform.webhooks.list(): { data: object[]; meta: api_meta; success: true; }`\n\n**get** `/platform/webhooks`\n\nGet all platform webhook endpoints configured for your organization.\n\nPlatform webhooks receive events from **all tenants** in your organization,\nunlike tenant webhooks which only receive events for a specific tenant.\nThis is useful for centralized event processing and monitoring.\n\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; }[]; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; url: string; }[]`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhooks = await client.platform.webhooks.list();\n\nconsole.log(webhooks);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.List', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhooks, err := client.Platform.Webhooks.List(context.TODO())\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhooks.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'platform.webhooks.list', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhooks = client.platform.webhooks.list()\nprint(webhooks.data)', + }, + ruby: { + method: 'platform.webhooks.list', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhooks = ark.platform.webhooks.list\n\nputs(webhooks)', + }, + typescript: { + method: 'client.platform.webhooks.list', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhooks = await client.platform.webhooks.list();\n\nconsole.log(webhooks.data);", + }, + }, + }, + { + name: 'retrieve', + endpoint: '/platform/webhooks/{webhookId}', + httpMethod: 'get', + summary: 'Get platform webhook details', + description: 'Get detailed information about a specific platform webhook.', + stainlessPath: '(resource) platform.webhooks > (method) retrieve', + qualified: 'client.platform.webhooks.retrieve', + params: ['webhookId: string;'], + response: + '{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve\n\n`client.platform.webhooks.retrieve(webhookId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/platform/webhooks/{webhookId}`\n\nGet detailed information about a specific platform webhook.\n\n### Parameters\n\n- `webhookId: string`\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.platform.webhooks.retrieve('pwh_abc123def456');\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Platform.Webhooks.Get(context.TODO(), "pwh_abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks/$WEBHOOK_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'platform.webhooks.retrieve', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.platform.webhooks.retrieve(\n "pwh_abc123def456",\n)\nprint(webhook.data)', + }, + ruby: { + method: 'platform.webhooks.retrieve', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.platform.webhooks.retrieve("pwh_abc123def456")\n\nputs(webhook)', + }, + typescript: { + method: 'client.platform.webhooks.retrieve', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.platform.webhooks.retrieve('pwh_abc123def456');\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'update', + endpoint: '/platform/webhooks/{webhookId}', + httpMethod: 'patch', + summary: 'Update a platform webhook', + description: + "Update a platform webhook's configuration.\n\nYou can update:\n- `name` - Display name for the webhook\n- `url` - The endpoint URL (must be HTTPS)\n- `events` - Array of event types to receive (empty array = all events)\n- `enabled` - Enable or disable the webhook\n", + stainlessPath: '(resource) platform.webhooks > (method) update', + qualified: 'client.platform.webhooks.update', + params: [ + 'webhookId: string;', + 'enabled?: boolean;', + 'events?: string[];', + 'name?: string;', + 'url?: string;', + ], + response: + '{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## update\n\n`client.platform.webhooks.update(webhookId: string, enabled?: boolean, events?: string[], name?: string, url?: string): { data: object; meta: api_meta; success: true; }`\n\n**patch** `/platform/webhooks/{webhookId}`\n\nUpdate a platform webhook's configuration.\n\nYou can update:\n- `name` - Display name for the webhook\n- `url` - The endpoint URL (must be HTTPS)\n- `events` - Array of event types to receive (empty array = all events)\n- `enabled` - Enable or disable the webhook\n\n\n### Parameters\n\n- `webhookId: string`\n\n- `enabled?: boolean`\n Enable or disable the webhook\n\n- `events?: string[]`\n Events to subscribe to. Empty array means all events.\n\n- `name?: string`\n Display name for the webhook\n\n- `url?: string`\n Webhook endpoint URL (must be HTTPS)\n\n### Returns\n\n- `{ data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; createdAt: string; enabled: boolean; events: string[]; name: string; updatedAt: string; url: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.platform.webhooks.update('pwh_abc123def456');\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.Update', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Platform.Webhooks.Update(\n\t\tcontext.TODO(),\n\t\t"pwh_abc123def456",\n\t\tark.PlatformWebhookUpdateParams{},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + "curl https://api.arkhq.io/v1/platform/webhooks/$WEBHOOK_ID \\\n -X PATCH \\\n -H 'Content-Type: application/json' \\\n -H \"Authorization: Bearer $ARK_API_KEY\" \\\n -d '{}'", + }, + python: { + method: 'platform.webhooks.update', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.platform.webhooks.update(\n webhook_id="pwh_abc123def456",\n)\nprint(webhook.data)', + }, + ruby: { + method: 'platform.webhooks.update', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.platform.webhooks.update("pwh_abc123def456")\n\nputs(webhook)', + }, + typescript: { + method: 'client.platform.webhooks.update', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.platform.webhooks.update('pwh_abc123def456');\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'delete', + endpoint: '/platform/webhooks/{webhookId}', + httpMethod: 'delete', + summary: 'Delete a platform webhook', + description: + 'Delete a platform webhook. This stops all event delivery to the webhook URL.\nThis action cannot be undone.\n', + stainlessPath: '(resource) platform.webhooks > (method) delete', + qualified: 'client.platform.webhooks.delete', + params: ['webhookId: string;'], + response: '{ data: { message: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## delete\n\n`client.platform.webhooks.delete(webhookId: string): { data: object; meta: api_meta; success: true; }`\n\n**delete** `/platform/webhooks/{webhookId}`\n\nDelete a platform webhook. This stops all event delivery to the webhook URL.\nThis action cannot be undone.\n\n\n### Parameters\n\n- `webhookId: string`\n\n### Returns\n\n- `{ data: { message: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { message: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst webhook = await client.platform.webhooks.delete('pwh_abc123def456');\n\nconsole.log(webhook);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.Delete', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\twebhook, err := client.Platform.Webhooks.Delete(context.TODO(), "pwh_abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", webhook.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks/$WEBHOOK_ID \\\n -X DELETE \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'platform.webhooks.delete', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nwebhook = client.platform.webhooks.delete(\n "pwh_abc123def456",\n)\nprint(webhook.data)', + }, + ruby: { + method: 'platform.webhooks.delete', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nwebhook = ark.platform.webhooks.delete("pwh_abc123def456")\n\nputs(webhook)', + }, + typescript: { + method: 'client.platform.webhooks.delete', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst webhook = await client.platform.webhooks.delete('pwh_abc123def456');\n\nconsole.log(webhook.data);", + }, + }, + }, + { + name: 'test', + endpoint: '/platform/webhooks/{webhookId}/test', + httpMethod: 'post', + summary: 'Test a platform webhook', + description: + 'Send a test payload to your platform webhook endpoint.\n\nUse this to:\n- Verify your webhook URL is accessible\n- Test your payload handling code\n- Ensure your server responds correctly\n\nThe test payload is marked with `_test: true` so you can distinguish\ntest events from real events.\n', + stainlessPath: '(resource) platform.webhooks > (method) test', + qualified: 'client.platform.webhooks.test', + params: ['webhookId: string;', 'event: string;'], + response: + '{ data: { durationMs: number; statusCode: number; success: boolean; error?: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## test\n\n`client.platform.webhooks.test(webhookId: string, event: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/platform/webhooks/{webhookId}/test`\n\nSend a test payload to your platform webhook endpoint.\n\nUse this to:\n- Verify your webhook URL is accessible\n- Test your payload handling code\n- Ensure your server responds correctly\n\nThe test payload is marked with `_test: true` so you can distinguish\ntest events from real events.\n\n\n### Parameters\n\n- `webhookId: string`\n\n- `event: string`\n Event type to simulate\n\n### Returns\n\n- `{ data: { durationMs: number; statusCode: number; success: boolean; error?: string; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { durationMs: number; statusCode: number; success: boolean; error?: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.platform.webhooks.test('pwh_abc123def456', { event: 'MessageSent' });\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.Test', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Platform.Webhooks.Test(\n\t\tcontext.TODO(),\n\t\t"pwh_abc123def456",\n\t\tark.PlatformWebhookTestParams{\n\t\t\tEvent: ark.PlatformWebhookTestParamsEventMessageSent,\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks/$WEBHOOK_ID/test \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $ARK_API_KEY" \\\n -d \'{\n "event": "MessageSent"\n }\'', + }, + python: { + method: 'platform.webhooks.test', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.platform.webhooks.test(\n webhook_id="pwh_abc123def456",\n event="MessageSent",\n)\nprint(response.data)', + }, + ruby: { + method: 'platform.webhooks.test_', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.platform.webhooks.test_("pwh_abc123def456", event: :MessageSent)\n\nputs(response)', + }, + typescript: { + method: 'client.platform.webhooks.test', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.platform.webhooks.test('pwh_abc123def456', { event: 'MessageSent' });\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'list_deliveries', + endpoint: '/platform/webhooks/deliveries', + httpMethod: 'get', + summary: 'List platform webhook deliveries', + description: + 'Get a paginated list of platform webhook delivery attempts.\n\nFilter by:\n- `webhookId` - Specific webhook\n- `tenantId` - Specific tenant\n- `event` - Specific event type\n- `success` - Successful (2xx) or failed deliveries\n- `before`/`after` - Time range (Unix timestamps)\n\nDeliveries are returned in reverse chronological order.\n', + stainlessPath: '(resource) platform.webhooks > (method) list_deliveries', + qualified: 'client.platform.webhooks.listDeliveries', + params: [ + 'after?: number;', + 'before?: number;', + 'event?: string;', + 'page?: number;', + 'perPage?: number;', + 'success?: boolean;', + 'tenantId?: string;', + 'webhookId?: string;', + ], + response: + '{ id: string; attempt: number; event: string; statusCode: number; success: boolean; tenantId: string; timestamp: string; url: string; webhookId: string; willRetry: boolean; }', + markdown: + "## list_deliveries\n\n`client.platform.webhooks.listDeliveries(after?: number, before?: number, event?: string, page?: number, perPage?: number, success?: boolean, tenantId?: string, webhookId?: string): { id: string; attempt: number; event: string; statusCode: number; success: boolean; tenantId: string; timestamp: string; url: string; webhookId: string; willRetry: boolean; }`\n\n**get** `/platform/webhooks/deliveries`\n\nGet a paginated list of platform webhook delivery attempts.\n\nFilter by:\n- `webhookId` - Specific webhook\n- `tenantId` - Specific tenant\n- `event` - Specific event type\n- `success` - Successful (2xx) or failed deliveries\n- `before`/`after` - Time range (Unix timestamps)\n\nDeliveries are returned in reverse chronological order.\n\n\n### Parameters\n\n- `after?: number`\n Only deliveries after this Unix timestamp\n\n- `before?: number`\n Only deliveries before this Unix timestamp\n\n- `event?: string`\n Filter by event type\n\n- `page?: number`\n Page number (default 1)\n\n- `perPage?: number`\n Items per page (default 30, max 100)\n\n- `success?: boolean`\n Filter by delivery success\n\n- `tenantId?: string`\n Filter by tenant ID\n\n- `webhookId?: string`\n Filter by platform webhook ID\n\n### Returns\n\n- `{ id: string; attempt: number; event: string; statusCode: number; success: boolean; tenantId: string; timestamp: string; url: string; webhookId: string; willRetry: boolean; }`\n Summary of a platform webhook delivery attempt\n\n - `id: string`\n - `attempt: number`\n - `event: string`\n - `statusCode: number`\n - `success: boolean`\n - `tenantId: string`\n - `timestamp: string`\n - `url: string`\n - `webhookId: string`\n - `willRetry: boolean`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\n// Automatically fetches more pages as needed.\nfor await (const webhookListDeliveriesResponse of client.platform.webhooks.listDeliveries()) {\n console.log(webhookListDeliveriesResponse);\n}\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.ListDeliveries', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpage, err := client.Platform.Webhooks.ListDeliveries(context.TODO(), ark.PlatformWebhookListDeliveriesParams{})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", page)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks/deliveries \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'platform.webhooks.list_deliveries', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\npage = client.platform.webhooks.list_deliveries()\npage = page.data[0]\nprint(page.id)', + }, + ruby: { + method: 'platform.webhooks.list_deliveries', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\npage = ark.platform.webhooks.list_deliveries\n\nputs(page)', + }, + typescript: { + method: 'client.platform.webhooks.listDeliveries', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\n// Automatically fetches more pages as needed.\nfor await (const webhookListDeliveriesResponse of client.platform.webhooks.listDeliveries()) {\n console.log(webhookListDeliveriesResponse.id);\n}", + }, + }, + }, + { + name: 'retrieve_delivery', + endpoint: '/platform/webhooks/deliveries/{deliveryId}', + httpMethod: 'get', + summary: 'Get platform webhook delivery details', + description: + 'Get detailed information about a specific platform webhook delivery.\n\nReturns the complete request payload, headers, response, and timing info.\n', + stainlessPath: '(resource) platform.webhooks > (method) retrieve_delivery', + qualified: 'client.platform.webhooks.retrieveDelivery', + params: ['deliveryId: string;'], + response: + '{ data: { id: string; attempt: number; event: string; request: { headers?: object; payload?: object; }; response: { body?: string; duration?: number; }; statusCode: number; success: boolean; tenantId: string; timestamp: string; url: string; webhookId: string; webhookName: string; willRetry: boolean; }; meta: { requestId: string; }; success: true; }', + markdown: + "## retrieve_delivery\n\n`client.platform.webhooks.retrieveDelivery(deliveryId: string): { data: object; meta: api_meta; success: true; }`\n\n**get** `/platform/webhooks/deliveries/{deliveryId}`\n\nGet detailed information about a specific platform webhook delivery.\n\nReturns the complete request payload, headers, response, and timing info.\n\n\n### Parameters\n\n- `deliveryId: string`\n\n### Returns\n\n- `{ data: { id: string; attempt: number; event: string; request: { headers?: object; payload?: object; }; response: { body?: string; duration?: number; }; statusCode: number; success: boolean; tenantId: string; timestamp: string; url: string; webhookId: string; webhookName: string; willRetry: boolean; }; meta: { requestId: string; }; success: true; }`\n\n - `data: { id: string; attempt: number; event: string; request: { headers?: object; payload?: object; }; response: { body?: string; duration?: number; }; statusCode: number; success: boolean; tenantId: string; timestamp: string; url: string; webhookId: string; webhookName: string; willRetry: boolean; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.platform.webhooks.retrieveDelivery('pwd_abc123def456');\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.GetDelivery', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Platform.Webhooks.GetDelivery(context.TODO(), "pwd_abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks/deliveries/$DELIVERY_ID \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'platform.webhooks.retrieve_delivery', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.platform.webhooks.retrieve_delivery(\n "pwd_abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'platform.webhooks.retrieve_delivery', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.platform.webhooks.retrieve_delivery("pwd_abc123def456")\n\nputs(response)', + }, + typescript: { + method: 'client.platform.webhooks.retrieveDelivery', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.platform.webhooks.retrieveDelivery('pwd_abc123def456');\n\nconsole.log(response.data);", + }, + }, + }, + { + name: 'replay_delivery', + endpoint: '/platform/webhooks/deliveries/{deliveryId}/replay', + httpMethod: 'post', + summary: 'Replay a platform webhook delivery', + description: + 'Replay a previous platform webhook delivery.\n\nThis re-sends the original payload with a new timestamp and delivery ID.\nUseful for recovering from temporary endpoint failures.\n', + stainlessPath: '(resource) platform.webhooks > (method) replay_delivery', + qualified: 'client.platform.webhooks.replayDelivery', + params: ['deliveryId: string;'], + response: + '{ data: { duration: number; newDeliveryId: string; originalDeliveryId: string; statusCode: number; success: boolean; timestamp: string; }; meta: { requestId: string; }; success: true; }', + markdown: + "## replay_delivery\n\n`client.platform.webhooks.replayDelivery(deliveryId: string): { data: object; meta: api_meta; success: true; }`\n\n**post** `/platform/webhooks/deliveries/{deliveryId}/replay`\n\nReplay a previous platform webhook delivery.\n\nThis re-sends the original payload with a new timestamp and delivery ID.\nUseful for recovering from temporary endpoint failures.\n\n\n### Parameters\n\n- `deliveryId: string`\n\n### Returns\n\n- `{ data: { duration: number; newDeliveryId: string; originalDeliveryId: string; statusCode: number; success: boolean; timestamp: string; }; meta: { requestId: string; }; success: true; }`\n Result of replaying a platform webhook delivery\n\n - `data: { duration: number; newDeliveryId: string; originalDeliveryId: string; statusCode: number; success: boolean; timestamp: string; }`\n - `meta: { requestId: string; }`\n - `success: true`\n\n### Example\n\n```typescript\nimport Ark from 'ark-email';\n\nconst client = new Ark();\n\nconst response = await client.platform.webhooks.replayDelivery('pwd_abc123def456');\n\nconsole.log(response);\n```", + perLanguage: { + go: { + method: 'client.Platform.Webhooks.ReplayDelivery', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tresponse, err := client.Platform.Webhooks.ReplayDelivery(context.TODO(), "pwd_abc123def456")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n', + }, + http: { + example: + 'curl https://api.arkhq.io/v1/platform/webhooks/deliveries/$DELIVERY_ID/replay \\\n -X POST \\\n -H "Authorization: Bearer $ARK_API_KEY"', + }, + python: { + method: 'platform.webhooks.replay_delivery', + example: + 'import os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\nresponse = client.platform.webhooks.replay_delivery(\n "pwd_abc123def456",\n)\nprint(response.data)', + }, + ruby: { + method: 'platform.webhooks.replay_delivery', + example: + 'require "ark_email"\n\nark = ArkEmail::Client.new(api_key: "My API Key")\n\nresponse = ark.platform.webhooks.replay_delivery("pwd_abc123def456")\n\nputs(response)', + }, + typescript: { + method: 'client.platform.webhooks.replayDelivery', + example: + "import Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.platform.webhooks.replayDelivery('pwd_abc123def456');\n\nconsole.log(response.data);", + }, + }, + }, +]; + +const EMBEDDED_READMES: { language: string; content: string }[] = [ + { + language: 'python', + content: + '# Ark Python API library\n\n\n[![PyPI version](https://img.shields.io/pypi/v/ark-email.svg?label=pypi%20(stable))](https://pypi.org/project/ark-email/)\n\nThe Ark Python library provides convenient access to the Ark REST API from any Python 3.9+\napplication. The library includes type definitions for all request params and response fields,\nand offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).\n\n\n\nIt is generated with [Stainless](https://www.stainless.com/).\n\n## MCP Server\n\nUse the Ark MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=ark-email-mcp&config=eyJuYW1lIjoiYXJrLWVtYWlsLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Fyay1tY3Auc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcmstYXBpLWtleSI6Ik15IEFQSSBLZXkifX0)\n[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22ark-email-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fark-mcp.stlmcp.com%22%2C%22headers%22%3A%7B%22x-ark-api-key%22%3A%22My%20API%20Key%22%7D%7D)\n\n> Note: You may need to set environment variables in your MCP client.\n\n## Documentation\n\nThe REST API documentation can be found on [arkhq.io](https://arkhq.io/docs). The full API of this library can be found in [api.md](api.md).\n\n## Installation\n\n```sh\n# install from PyPI\npip install ark-email\n```\n\n## Usage\n\nThe full API of this library can be found in [api.md](api.md).\n\n```python\nimport os\nfrom ark import Ark\n\nclient = Ark(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\n\nresponse = client.emails.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n)\nprint(response.data)\n```\n\nWhile you can provide an `api_key` keyword argument,\nwe recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)\nto add `ARK_API_KEY="My API Key"` to your `.env` file\nso that your API Key is not stored in source control.\n\n## Async usage\n\nSimply import `AsyncArk` instead of `Ark` and use `await` with each API call:\n\n```python\nimport os\nimport asyncio\nfrom ark import AsyncArk\n\nclient = AsyncArk(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n)\n\nasync def main() -> None:\n response = await client.emails.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n )\n print(response.data)\n\nasyncio.run(main())\n```\n\nFunctionality between the synchronous and asynchronous clients is otherwise identical.\n\n### With aiohttp\n\nBy default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.\n\nYou can enable this by installing `aiohttp`:\n\n```sh\n# install from PyPI\npip install ark-email[aiohttp]\n```\n\nThen you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:\n\n```python\nimport os\nimport asyncio\nfrom ark import DefaultAioHttpClient\nfrom ark import AsyncArk\n\nasync def main() -> None:\n async with AsyncArk(\n api_key=os.environ.get("ARK_API_KEY"), # This is the default and can be omitted\n http_client=DefaultAioHttpClient(),\n) as client:\n response = await client.emails.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n )\n print(response.data)\n\nasyncio.run(main())\n```\n\n\n\n## Using types\n\nNested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like:\n\n- Serializing back into JSON, `model.to_json()`\n- Converting to a dictionary, `model.to_dict()`\n\nTyped requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.\n\n## Pagination\n\nList methods in the Ark API are paginated.\n\nThis library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:\n\n```python\nfrom ark import Ark\n\nclient = Ark()\n\nall_emails = []\n# Automatically fetches more pages as needed.\nfor email in client.emails.list(\n page=1,\n per_page=10,\n):\n # Do something with email here\n all_emails.append(email)\nprint(all_emails)\n```\n\nOr, asynchronously:\n\n```python\nimport asyncio\nfrom ark import AsyncArk\n\nclient = AsyncArk()\n\nasync def main() -> None:\n all_emails = []\n # Iterate through items across all pages, issuing requests as needed.\n async for email in client.emails.list(\n page=1,\n per_page=10,\n):\n all_emails.append(email)\n print(all_emails)\n\nasyncio.run(main())\n```\n\nAlternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:\n\n```python\nfirst_page = await client.emails.list(\n page=1,\n per_page=10,\n)\nif first_page.has_next_page():\n print(f"will fetch next page using these details: {first_page.next_page_info()}")\n next_page = await first_page.get_next_page()\n print(f"number of items we just fetched: {len(next_page.data)}")\n\n# Remove `await` for non-async usage.\n```\n\nOr just work directly with the returned data:\n\n```python\nfirst_page = await client.emails.list(\n page=1,\n per_page=10,\n)\n\nprint(f"page number: {first_page.page}") # => "page number: 1"\nfor email in first_page.data:\n print(email.id)\n\n# Remove `await` for non-async usage.\n```\n\n\n\n\n\n## Handling errors\n\nWhen the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `ark.APIConnectionError` is raised.\n\nWhen the API returns a non-success status code (that is, 4xx or 5xx\nresponse), a subclass of `ark.APIStatusError` is raised, containing `status_code` and `response` properties.\n\nAll errors inherit from `ark.APIError`.\n\n```python\nimport ark\nfrom ark import Ark\n\nclient = Ark()\n\ntry:\n client.emails.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n )\nexcept ark.APIConnectionError as e:\n print("The server could not be reached")\n print(e.__cause__) # an underlying Exception, likely raised within httpx.\nexcept ark.RateLimitError as e:\n print("A 429 status code was received; we should back off a bit.")\nexcept ark.APIStatusError as e:\n print("Another non-200-range status code was received")\n print(e.status_code)\n print(e.response)\n```\n\nError codes are as follows:\n\n| Status Code | Error Type |\n| ----------- | -------------------------- |\n| 400 | `BadRequestError` |\n| 401 | `AuthenticationError` |\n| 403 | `PermissionDeniedError` |\n| 404 | `NotFoundError` |\n| 422 | `UnprocessableEntityError` |\n| 429 | `RateLimitError` |\n| >=500 | `InternalServerError` |\n| N/A | `APIConnectionError` |\n\n### Retries\n\nCertain errors are automatically retried 2 times by default, with a short exponential backoff.\nConnection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,\n429 Rate Limit, and >=500 Internal errors are all retried by default.\n\nYou can use the `max_retries` option to configure or disable retry settings:\n\n```python\nfrom ark import Ark\n\n# Configure the default for all requests:\nclient = Ark(\n # default is 2\n max_retries=0,\n)\n\n# Or, configure per-request:\nclient.with_options(max_retries = 5).emails.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n)\n```\n\n### Timeouts\n\nBy default requests time out after 1 minute. You can configure this with a `timeout` option,\nwhich accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:\n\n```python\nfrom ark import Ark\n\n# Configure the default for all requests:\nclient = Ark(\n # 20 seconds (default is 1 minute)\n timeout=20.0,\n)\n\n# More granular control:\nclient = Ark(\n timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0),\n)\n\n# Override per-request:\nclient.with_options(timeout = 5.0).emails.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n)\n```\n\nOn timeout, an `APITimeoutError` is thrown.\n\nNote that requests that time out are [retried twice by default](#retries).\n\n\n\n## Advanced\n\n### Logging\n\nWe use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module.\n\nYou can enable logging by setting the environment variable `ARK_LOG` to `info`.\n\n```shell\n$ export ARK_LOG=info\n```\n\nOr to `debug` for more verbose logging.\n\n### How to tell whether `None` means `null` or missing\n\nIn an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:\n\n```py\nif response.my_field is None:\n if \'my_field\' not in response.model_fields_set:\n print(\'Got json like {}, without a "my_field" key present at all.\')\n else:\n print(\'Got json like {"my_field": null}.\')\n```\n\n### Accessing raw response data (e.g. headers)\n\nThe "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g.,\n\n```py\nfrom ark import Ark\n\nclient = Ark()\nresponse = client.emails.with_raw_response.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n)\nprint(response.headers.get(\'X-My-Header\'))\n\nemail = response.parse() # get the object that `emails.send()` would have returned\nprint(email.data)\n```\n\nThese methods return an [`APIResponse`](https://github.com/ArkHQ-io/ark-python/tree/main/src/ark/_response.py) object.\n\nThe async client returns an [`AsyncAPIResponse`](https://github.com/ArkHQ-io/ark-python/tree/main/src/ark/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.\n\n#### `.with_streaming_response`\n\nThe above interface eagerly reads the full response body when you make the request, which may not always be what you want.\n\nTo stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods.\n\n```python\nwith client.emails.with_streaming_response.send(\n from_="hello@yourdomain.com",\n subject="Hello World",\n to=["user@example.com"],\n html="

Welcome!

",\n metadata={\n "user_id": "usr_123456",\n "campaign": "onboarding",\n },\n tag="welcome",\n) as response :\n print(response.headers.get(\'X-My-Header\'))\n\n for line in response.iter_lines():\n print(line)\n```\n\nThe context manager is required so that the response will reliably be closed.\n\n### Making custom/undocumented requests\n\nThis library is typed for convenient access to the documented API.\n\nIf you need to access undocumented endpoints, params, or response properties, the library can still be used.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other\nhttp verbs. Options on the client will be respected (such as retries) when making this request.\n\n```py\nimport httpx\n\nresponse = client.post(\n "/foo",\n cast_to=httpx.Response,\n body={"my_param": True},\n)\n\nprint(response.headers.get("x-foo"))\n```\n\n#### Undocumented request params\n\nIf you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request\noptions.\n\n#### Undocumented response properties\n\nTo access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You\ncan also get all the extra fields on the Pydantic model as a dict with\n[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra).\n\n### Configuring the HTTP client\n\nYou can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including:\n\n- Support for [proxies](https://www.python-httpx.org/advanced/proxies/)\n- Custom [transports](https://www.python-httpx.org/advanced/transports/)\n- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality\n\n```python\nimport httpx\nfrom ark import Ark, DefaultHttpxClient\n\nclient = Ark(\n # Or use the `ARK_BASE_URL` env var\n base_url="http://my.test.server.example.com:8083",\n http_client=DefaultHttpxClient(proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0")),\n)\n```\n\nYou can also customize the client on a per-request basis by using `with_options()`:\n\n```python\nclient.with_options(http_client=DefaultHttpxClient(...))\n```\n\n### Managing HTTP resources\n\nBy default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.\n\n```py\nfrom ark import Ark\n\nwith Ark() as client:\n # make requests here\n ...\n\n# HTTP client is now closed\n```\n\n## Versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes that only affect static types, without breaking runtime behavior.\n2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n3. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/ArkHQ-io/ark-python/issues) with questions, bugs, or suggestions.\n\n### Determining the installed version\n\nIf you\'ve upgraded to the latest version but aren\'t seeing any new features you were expecting then your python environment is likely still using an older version.\n\nYou can determine the version that is being used at runtime with:\n\n```py\nimport ark\nprint(ark.__version__)\n```\n\n## Requirements\n\nPython 3.9 or higher.\n\n## Contributing\n\nSee [the contributing documentation](./CONTRIBUTING.md).\n', + }, + { + language: 'go', + content: + '# Ark Go API Library\n\nGo Reference\n\nThe Ark Go library provides convenient access to the [Ark REST API](https://arkhq.io/docs)\nfrom applications written in Go.\n\nIt is generated with [Stainless](https://www.stainless.com/).\n\n## MCP Server\n\nUse the Ark MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=ark-email-mcp&config=eyJuYW1lIjoiYXJrLWVtYWlsLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Fyay1tY3Auc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcmstYXBpLWtleSI6Ik15IEFQSSBLZXkifX0)\n[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22ark-email-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fark-mcp.stlmcp.com%22%2C%22headers%22%3A%7B%22x-ark-api-key%22%3A%22My%20API%20Key%22%7D%7D)\n\n> Note: You may need to set environment variables in your MCP client.\n\n## Installation\n\n\n\n```go\nimport (\n\t"github.com/ArkHQ-io/ark-go" // imported as SDK_PackageName\n)\n```\n\n\n\nOr to pin the version:\n\n\n\n```sh\ngo get -u \'github.com/ArkHQ-io/ark-go@v0.0.1\'\n```\n\n\n\n## Requirements\n\nThis library requires Go 1.22+.\n\n## Usage\n\nThe full API of this library can be found in [api.md](api.md).\n\n```go\npackage main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/ArkHQ-io/ark-go"\n\t"github.com/ArkHQ-io/ark-go/option"\n)\n\nfunc main() {\n\tclient := ark.NewClient(\n\t\toption.WithAPIKey("My API Key"), // defaults to os.LookupEnv("ARK_API_KEY")\n\t)\n\tresponse, err := client.Emails.Send(context.TODO(), ark.EmailSendParams{\n\t\tFrom: "hello@yourdomain.com",\n\t\tSubject: "Hello World",\n\t\tTo: []string{"user@example.com"},\n\t\tHTML: ark.String("

Welcome!

"),\n\t\tMetadata: map[string]string{\n\t\t\t"user_id": "usr_123456",\n\t\t\t"campaign": "onboarding",\n\t\t},\n\t\tTag: ark.String("welcome"),\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", response.Data)\n}\n\n```\n\n### Request fields\n\nAll request parameters are wrapped in a generic `Field` type,\nwhich we use to distinguish zero values from null or omitted fields.\n\nThis prevents accidentally sending a zero value if you forget a required parameter,\nand enables explicitly sending `null`, `false`, `\'\'`, or `0` on optional parameters.\nAny field not specified is not sent.\n\nTo construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`.\nTo send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example:\n\n```go\nparams := FooParams{\n\tName: SDK_PackageName.F("hello"),\n\n\t// Explicitly send `"description": null`\n\tDescription: SDK_PackageName.Null[string](),\n\n\tPoint: SDK_PackageName.F(SDK_PackageName.Point{\n\t\tX: SDK_PackageName.Int(0),\n\t\tY: SDK_PackageName.Int(1),\n\n\t\t// In cases where the API specifies a given type,\n\t\t// but you want to send something else, use `Raw`:\n\t\tZ: SDK_PackageName.Raw[int64](0.01), // sends a float\n\t}),\n}\n```\n\n### Response objects\n\nAll fields in response structs are value types (not pointers or wrappers).\n\nIf a given field is `null`, not present, or invalid, the corresponding field\nwill simply be its zero value.\n\nAll response structs also include a special `JSON` field, containing more detailed\ninformation about each property, which you can use like so:\n\n```go\nif res.Name == "" {\n\t// true if `"name"` is either not present or explicitly null\n\tres.JSON.Name.IsNull()\n\n\t// true if the `"name"` key was not present in the response JSON at all\n\tres.JSON.Name.IsMissing()\n\n\t// When the API returns data that cannot be coerced to the expected type:\n\tif res.JSON.Name.IsInvalid() {\n\t\traw := res.JSON.Name.Raw()\n\n\t\tlegacyName := struct{\n\t\t\tFirst string `json:"first"`\n\t\t\tLast string `json:"last"`\n\t\t}{}\n\t\tjson.Unmarshal([]byte(raw), &legacyName)\n\t\tname = legacyName.First + " " + legacyName.Last\n\t}\n}\n```\n\nThese `.JSON` structs also include an `Extras` map containing\nany properties in the json response that were not specified\nin the struct. This can be useful for API features not yet\npresent in the SDK.\n\n```go\nbody := res.JSON.ExtraFields["my_unexpected_field"].Raw()\n```\n\n### RequestOptions\n\nThis library uses the functional options pattern. Functions defined in the\n`SDK_PackageOptionName` package return a `RequestOption`, which is a closure that mutates a\n`RequestConfig`. These options can be supplied to the client or at individual\nrequests. For example:\n\n```go\nclient := SDK_PackageName.SDK_ClientInitializerName(\n\t// Adds a header to every request made by the client\n\tSDK_PackageOptionName.WithHeader("X-Some-Header", "custom_header_info"),\n)\n\nclient.Emails.Send(context.TODO(), ...,\n\t// Override the header\n\tSDK_PackageOptionName.WithHeader("X-Some-Header", "some_other_custom_header_info"),\n\t// Add an undocumented field to the request body, using sjson syntax\n\tSDK_PackageOptionName.WithJSONSet("some.json.path", map[string]string{"my": "object"}),\n)\n```\n\nSee the [full list of request options](https://pkg.go.dev/github.com/ArkHQ-io/ark-go/SDK_PackageOptionName).\n\n### Pagination\n\nThis library provides some conveniences for working with paginated list endpoints.\n\nYou can use `.ListAutoPaging()` methods to iterate through items across all pages:\n\n```go\niter := client.Emails.ListAutoPaging(context.TODO(), ark.EmailListParams{\n\tPage: ark.Int(1),\n\tPerPage: ark.Int(10),\n})\n// Automatically fetches more pages as needed.\nfor iter.Next() {\n\temailListResponse := iter.Current()\n\tfmt.Printf("%+v\\n", emailListResponse)\n}\nif err := iter.Err(); err != nil {\n\tpanic(err.Error())\n}\n```\n\nOr you can use simple `.List()` methods to fetch a single page and receive a standard response object\nwith additional helper methods like `.GetNextPage()`, e.g.:\n\n```go\npage, err := client.Emails.List(context.TODO(), ark.EmailListParams{\n\tPage: ark.Int(1),\n\tPerPage: ark.Int(10),\n})\nfor page != nil {\n\tfor _, email := range page.Data {\n\t\tfmt.Printf("%+v\\n", email)\n\t}\n\tpage, err = page.GetNextPage()\n}\nif err != nil {\n\tpanic(err.Error())\n}\n```\n\n### Errors\n\nWhen the API returns a non-success status code, we return an error with type\n`*SDK_PackageName.Error`. This contains the `StatusCode`, `*http.Request`, and\n`*http.Response` values of the request, as well as the JSON of the error body\n(much like other response objects in the SDK).\n\nTo handle errors, we recommend that you use the `errors.As` pattern:\n\n```go\n_, err := client.Emails.Send(context.TODO(), ark.EmailSendParams{\n\tFrom: "hello@yourdomain.com",\n\tSubject: "Hello World",\n\tTo: []string{"user@example.com"},\n\tHTML: ark.String("

Welcome!

"),\n\tMetadata: map[string]string{\n\t\t"user_id": "usr_123456",\n\t\t"campaign": "onboarding",\n\t},\n\tTag: ark.String("welcome"),\n})\nif err != nil {\n\tvar apierr *ark.Error\n\tif errors.As(err, &apierr) {\n\t\tprintln(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request\n\t\tprintln(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response\n\t}\n\tpanic(err.Error()) // GET "/emails": 400 Bad Request { ... }\n}\n```\n\nWhen other errors occur, they are returned unwrapped; for example,\nif HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.\n\n### Timeouts\n\nRequests do not time out by default; use context to configure a timeout for a request lifecycle.\n\nNote that if a request is [retried](#retries), the context timeout does not start over.\nTo set a per-retry timeout, use `SDK_PackageOptionName.WithRequestTimeout()`.\n\n```go\n// This sets the timeout for the request, including all the retries.\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\ndefer cancel()\nclient.Emails.Send(\n\tctx,\n\tark.EmailSendParams{\n\t\tFrom: "hello@yourdomain.com",\n\t\tSubject: "Hello World",\n\t\tTo: []string{"user@example.com"},\n\t\tHTML: ark.String("

Welcome!

"),\n\t\tMetadata: map[string]string{\n\t\t\t"user_id": "usr_123456",\n\t\t\t"campaign": "onboarding",\n\t\t},\n\t\tTag: ark.String("welcome"),\n\t},\n\t// This sets the per-retry timeout\n\toption.WithRequestTimeout(20*time.Second),\n)\n```\n\n### File uploads\n\nRequest parameters that correspond to file uploads in multipart requests are typed as\n`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form\npart with the file name of "anonymous_file" and content-type of "application/octet-stream".\n\nThe file name and content-type can be customized by implementing `Name() string` or `ContentType()\nstring` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a\nfile returned by `os.Open` will be sent with the file name on disk.\n\nWe also provide a helper `SDK_PackageName.FileParam(reader io.Reader, filename string, contentType string)`\nwhich can be used to wrap any `io.Reader` with the appropriate file name and content type.\n\n\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\nWe retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,\nand >=500 Internal errors.\n\nYou can use the `WithMaxRetries` option to configure or disable this:\n\n```go\n// Configure the default for all requests:\nclient := ark.NewClient(\n\toption.WithMaxRetries(0), // default is 2\n)\n\n// Override per-request:\nclient.Emails.Send(\n\tcontext.TODO(),\n\tark.EmailSendParams{\n\t\tFrom: "hello@yourdomain.com",\n\t\tSubject: "Hello World",\n\t\tTo: []string{"user@example.com"},\n\t\tHTML: ark.String("

Welcome!

"),\n\t\tMetadata: map[string]string{\n\t\t\t"user_id": "usr_123456",\n\t\t\t"campaign": "onboarding",\n\t\t},\n\t\tTag: ark.String("welcome"),\n\t},\n\toption.WithMaxRetries(5),\n)\n```\n\n\n### Accessing raw response data (e.g. response headers)\n\nYou can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when\nyou need to examine response headers, status codes, or other details.\n\n```go\n// Create a variable to store the HTTP response\nvar response *http.Response\nresponse, err := client.Emails.Send(\n\tcontext.TODO(),\n\tark.EmailSendParams{\n\t\tFrom: "hello@yourdomain.com",\n\t\tSubject: "Hello World",\n\t\tTo: []string{"user@example.com"},\n\t\tHTML: ark.String("

Welcome!

"),\n\t\tMetadata: map[string]string{\n\t\t\t"user_id": "usr_123456",\n\t\t\t"campaign": "onboarding",\n\t\t},\n\t\tTag: ark.String("welcome"),\n\t},\n\toption.WithResponseInto(&response),\n)\nif err != nil {\n\t// handle error\n}\nfmt.Printf("%+v\\n", response)\n\nfmt.Printf("Status Code: %d\\n", response.StatusCode)\nfmt.Printf("Headers: %+#v\\n", response.Header)\n```\n\n### Making custom/undocumented requests\n\nThis library is typed for convenient access to the documented API. If you need to access undocumented\nendpoints, params, or response properties, the library can still be used.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs.\n`RequestOptions` on the client, such as retries, will be respected when making these requests.\n\n```go\nvar (\n // params can be an io.Reader, a []byte, an encoding/json serializable object,\n // or a "…Params" struct defined in this library.\n params map[string]interface{}\n\n // result can be an []byte, *http.Response, a encoding/json deserializable object,\n // or a model defined in this library.\n result *http.Response\n)\nerr := client.Post(context.Background(), "/unspecified", params, &result)\nif err != nil {\n …\n}\n```\n\n#### Undocumented request params\n\nTo make requests using undocumented parameters, you may use either the `SDK_PackageOptionName.WithQuerySet()`\nor the `SDK_PackageOptionName.WithJSONSet()` methods.\n\n```go\nparams := FooNewParams{\n ID: SDK_PackageName.F("id_xxxx"),\n Data: SDK_PackageName.F(FooNewParamsData{\n FirstName: SDK_PackageName.F("John"),\n }),\n}\nclient.Foo.New(context.Background(), params, SDK_PackageOptionName.WithJSONSet("data.last_name", "Doe"))\n```\n\n#### Undocumented response properties\n\nTo access undocumented response properties, you may either access the raw JSON of the response as a string\nwith `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with\n`result.JSON.Foo.Raw()`.\n\nAny fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.\n\n### Middleware\n\nWe provide `SDK_PackageOptionName.WithMiddleware` which applies the given\nmiddleware to requests.\n\n```go\nfunc Logger(req *http.Request, next SDK_PackageOptionName.MiddlewareNext) (res *http.Response, err error) {\n\t// Before the request\n\tstart := time.Now()\n\tLogReq(req)\n\n\t// Forward the request to the next handler\n\tres, err = next(req)\n\n\t// Handle stuff after the request\n\tend := time.Now()\n\tLogRes(res, err, start - end)\n\n return res, err\n}\n\nclient := SDK_PackageName.SDK_ClientInitializerName(\n\tSDK_PackageOptionName.WithMiddleware(Logger),\n)\n```\n\nWhen multiple middlewares are provided as variadic arguments, the middlewares\nare applied left to right. If `SDK_PackageOptionName.WithMiddleware` is given\nmultiple times, for example first in the client then the method, the\nmiddleware in the client will run first and the middleware given in the method\nwill run next.\n\nYou may also replace the default `http.Client` with\n`SDK_PackageOptionName.WithHTTPClient(client)`. Only one http client is\naccepted (this overwrites any previous client) and receives requests after any\nmiddleware has been applied.\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n2. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/ArkHQ-io/ark-go/issues) with questions, bugs, or suggestions.\n\n## Contributing\n\nSee [the contributing documentation](./CONTRIBUTING.md).\n', + }, + { + language: 'typescript', + content: + "# Ark TypeScript API Library\n\n[![NPM version](https://img.shields.io/npm/v/ark-email.svg?label=npm%20(stable))](https://npmjs.org/package/ark-email) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/ark-email)\n\nThis library provides convenient access to the Ark REST API from server-side TypeScript or JavaScript.\n\n\n\nThe REST API documentation can be found on [arkhq.io](https://arkhq.io/docs). The full API of this library can be found in [api.md](api.md).\n\nIt is generated with [Stainless](https://www.stainless.com/).\n\n## MCP Server\n\nUse the Ark MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=ark-email-mcp&config=eyJuYW1lIjoiYXJrLWVtYWlsLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Fyay1tY3Auc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcmstYXBpLWtleSI6Ik15IEFQSSBLZXkifX0)\n[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22ark-email-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fark-mcp.stlmcp.com%22%2C%22headers%22%3A%7B%22x-ark-api-key%22%3A%22My%20API%20Key%22%7D%7D)\n\n> Note: You may need to set environment variables in your MCP client.\n\n## Installation\n\n```sh\nnpm install ark-email\n```\n\n\n\n## Usage\n\nThe full API of this library can be found in [api.md](api.md).\n\n\n```js\nimport Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.emails.send({\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n});\n\nconsole.log(response.data);\n```\n\n\n\n### Request & Response types\n\nThis library includes TypeScript definitions for all request params and response fields. You may import and use them like so:\n\n\n```ts\nimport Ark from 'ark-email';\n\nconst client = new Ark({\n apiKey: process.env['ARK_API_KEY'], // This is the default and can be omitted\n});\n\nconst params: Ark.EmailSendParams = {\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n};\nconst response: Ark.EmailSendResponse = await client.emails.send(params);\n```\n\nDocumentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors.\n\n\n\n\n\n## Handling errors\n\nWhen the library is unable to connect to the API,\nor if the API returns a non-success status code (i.e., 4xx or 5xx response),\na subclass of `APIError` will be thrown:\n\n\n```ts\nconst response = await client.emails\n .send({\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n })\n .catch(async (err) => {\n if (err instanceof Ark.APIError) {\n console.log(err.status); // 400\n console.log(err.name); // BadRequestError\n console.log(err.headers); // {server: 'nginx', ...}\n } else {\n throw err;\n }\n });\n```\n\nError codes are as follows:\n\n| Status Code | Error Type |\n| ----------- | -------------------------- |\n| 400 | `BadRequestError` |\n| 401 | `AuthenticationError` |\n| 403 | `PermissionDeniedError` |\n| 404 | `NotFoundError` |\n| 422 | `UnprocessableEntityError` |\n| 429 | `RateLimitError` |\n| >=500 | `InternalServerError` |\n| N/A | `APIConnectionError` |\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\nConnection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,\n429 Rate Limit, and >=500 Internal errors will all be retried by default.\n\nYou can use the `maxRetries` option to configure or disable this:\n\n\n```js\n// Configure the default for all requests:\nconst client = new Ark({\n maxRetries: 0, // default is 2\n});\n\n// Or, configure per-request:\nawait client.emails.send({\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n}, {\n maxRetries: 5,\n});\n```\n\n### Timeouts\n\nRequests time out after 1 minute by default. You can configure this with a `timeout` option:\n\n\n```ts\n// Configure the default for all requests:\nconst client = new Ark({\n timeout: 20 * 1000, // 20 seconds (default is 1 minute)\n});\n\n// Override per-request:\nawait client.emails.send({\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n}, {\n timeout: 5 * 1000,\n});\n```\n\nOn timeout, an `APIConnectionTimeoutError` is thrown.\n\nNote that requests which time out will be [retried twice by default](#retries).\n\n## Auto-pagination\n\nList methods in the Ark API are paginated.\nYou can use the `for await … of` syntax to iterate through items across all pages:\n\n```ts\nasync function fetchAllEmailListResponses(params) {\n const allEmailListResponses = [];\n // Automatically fetches more pages as needed.\n for await (const emailListResponse of client.emails.list({ page: 1, perPage: 10 })) {\n allEmailListResponses.push(emailListResponse);\n }\n return allEmailListResponses;\n}\n```\n\nAlternatively, you can request a single page at a time:\n\n```ts\nlet page = await client.emails.list({ page: 1, perPage: 10 });\nfor (const emailListResponse of page.data) {\n console.log(emailListResponse);\n}\n\n// Convenience methods are provided for manually paginating:\nwhile (page.hasNextPage()) {\n page = await page.getNextPage();\n // ...\n}\n```\n\n\n\n## Advanced Usage\n\n### Accessing raw Response data (e.g., headers)\n\nThe \"raw\" `Response` returned by `fetch()` can be accessed through the `.asResponse()` method on the `APIPromise` type that all methods return.\nThis method returns as soon as the headers for a successful response are received and does not consume the response body, so you are free to write custom parsing or streaming logic.\n\nYou can also use the `.withResponse()` method to get the raw `Response` along with the parsed data.\nUnlike `.asResponse()` this method consumes the body, returning once it is parsed.\n\n\n```ts\nconst client = new Ark();\n\nconst response = await client.emails\n .send({\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n })\n .asResponse();\nconsole.log(response.headers.get('X-My-Header'));\nconsole.log(response.statusText); // access the underlying Response object\n\nconst { data: response, response: raw } = await client.emails\n .send({\n from: 'hello@yourdomain.com',\n subject: 'Hello World',\n to: ['user@example.com'],\n html: '

Welcome!

',\n metadata: { user_id: 'usr_123456', campaign: 'onboarding' },\n tag: 'welcome',\n })\n .withResponse();\nconsole.log(raw.headers.get('X-My-Header'));\nconsole.log(response.data);\n```\n\n### Logging\n\n> [!IMPORTANT]\n> All log messages are intended for debugging only. The format and content of log messages\n> may change between releases.\n\n#### Log levels\n\nThe log level can be configured in two ways:\n\n1. Via the `ARK_LOG` environment variable\n2. Using the `logLevel` client option (overrides the environment variable if set)\n\n```ts\nimport Ark from 'ark-email';\n\nconst client = new Ark({\n logLevel: 'debug', // Show all log messages\n});\n```\n\nAvailable log levels, from most to least verbose:\n\n- `'debug'` - Show debug messages, info, warnings, and errors\n- `'info'` - Show info messages, warnings, and errors\n- `'warn'` - Show warnings and errors (default)\n- `'error'` - Show only errors\n- `'off'` - Disable all logging\n\nAt the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies.\nSome authentication-related headers are redacted, but sensitive data in request and response bodies\nmay still be visible.\n\n#### Custom logger\n\nBy default, this library logs to `globalThis.console`. You can also provide a custom logger.\nMost logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue.\n\nWhen providing a custom logger, the `logLevel` option still controls which messages are emitted, messages\nbelow the configured level will not be sent to your logger.\n\n```ts\nimport Ark from 'ark-email';\nimport pino from 'pino';\n\nconst logger = pino();\n\nconst client = new Ark({\n logger: logger.child({ name: 'Ark' }),\n logLevel: 'debug', // Send all messages to pino, allowing it to filter\n});\n```\n\n### Making custom/undocumented requests\n\nThis library is typed for convenient access to the documented API. If you need to access undocumented\nendpoints, params, or response properties, the library can still be used.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints, you can use `client.get`, `client.post`, and other HTTP verbs.\nOptions on the client, such as retries, will be respected when making these requests.\n\n```ts\nawait client.post('/some/path', {\n body: { some_prop: 'foo' },\n query: { some_query_arg: 'bar' },\n});\n```\n\n#### Undocumented request params\n\nTo make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented\nparameter. This library doesn't validate at runtime that the request matches the type, so any extra values you\nsend will be sent as-is.\n\n```ts\nclient.emails.send({\n // ...\n // @ts-expect-error baz is not yet public\n baz: 'undocumented option',\n});\n```\n\nFor requests with the `GET` verb, any extra params will be in the query, all other requests will send the\nextra param in the body.\n\nIf you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request\noptions.\n\n#### Undocumented response properties\n\nTo access undocumented response properties, you may access the response object with `// @ts-expect-error` on\nthe response object, or cast the response object to the requisite type. Like the request params, we do not\nvalidate or strip extra properties from the response from the API.\n\n### Customizing the fetch client\n\nBy default, this library expects a global `fetch` function is defined.\n\nIf you want to use a different `fetch` function, you can either polyfill the global:\n\n```ts\nimport fetch from 'my-fetch';\n\nglobalThis.fetch = fetch;\n```\n\nOr pass it to the client:\n\n```ts\nimport Ark from 'ark-email';\nimport fetch from 'my-fetch';\n\nconst client = new Ark({ fetch });\n```\n\n### Fetch options\n\nIf you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.)\n\n```ts\nimport Ark from 'ark-email';\n\nconst client = new Ark({\n fetchOptions: {\n // `RequestInit` options\n },\n});\n```\n\n#### Configuring proxies\n\nTo modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy\noptions to requests:\n\n **Node** [[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)]\n\n```ts\nimport Ark from 'ark-email';\nimport * as undici from 'undici';\n\nconst proxyAgent = new undici.ProxyAgent('http://localhost:8888');\nconst client = new Ark({\n fetchOptions: {\n dispatcher: proxyAgent,\n },\n});\n```\n\n **Bun** [[docs](https://bun.sh/guides/http/proxy)]\n\n```ts\nimport Ark from 'ark-email';\n\nconst client = new Ark({\n fetchOptions: {\n proxy: 'http://localhost:8888',\n },\n});\n```\n\n **Deno** [[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)]\n\n```ts\nimport Ark from 'npm:ark-email';\n\nconst httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } });\nconst client = new Ark({\n fetchOptions: {\n client: httpClient,\n },\n});\n```\n\n## Frequently Asked Questions\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes that only affect static types, without breaking runtime behavior.\n2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n3. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/ArkHQ-io/ark-nodejs/issues) with questions, bugs, or suggestions.\n\n## Requirements\n\nTypeScript >= 4.9 is supported.\n\nThe following runtimes are supported:\n\n- Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more)\n- Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions.\n- Deno v1.28.0 or higher.\n- Bun 1.0 or later.\n- Cloudflare Workers.\n- Vercel Edge Runtime.\n- Jest 28 or greater with the `\"node\"` environment (`\"jsdom\"` is not supported at this time).\n- Nitro v2.6 or greater.\n\nNote that React Native is not supported at this time.\n\nIf you are interested in other runtime environments, please open or upvote an issue on GitHub.\n\n## Contributing\n\nSee [the contributing documentation](./CONTRIBUTING.md).\n", + }, + { + language: 'ruby', + content: + '# Ark Ruby API library\n\nThe Ark Ruby library provides convenient access to the Ark REST API from any Ruby 3.2.0+ application. It ships with comprehensive types & docstrings in Yard, RBS, and RBI – [see below](https://github.com/ArkHQ-io/ark-ruby#Sorbet) for usage with Sorbet. The standard library\'s `net/http` is used as the HTTP transport, with connection pooling via the `connection_pool` gem.\n\n\n\nIt is generated with [Stainless](https://www.stainless.com/).\n\n## MCP Server\n\nUse the Ark MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=ark-email-mcp&config=eyJuYW1lIjoiYXJrLWVtYWlsLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Fyay1tY3Auc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcmstYXBpLWtleSI6Ik15IEFQSSBLZXkifX0)\n[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22ark-email-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fark-mcp.stlmcp.com%22%2C%22headers%22%3A%7B%22x-ark-api-key%22%3A%22My%20API%20Key%22%7D%7D)\n\n> Note: You may need to set environment variables in your MCP client.\n\n## Documentation\n\nDocumentation for releases of this gem can be found [on RubyDoc](https://gemdocs.org/gems/ark-email).\n\nThe REST API documentation can be found on [arkhq.io](https://arkhq.io/docs).\n\n## Installation\n\nTo use this gem, install via Bundler by adding the following to your application\'s `Gemfile`:\n\n\n\n```ruby\ngem "ark-email", "~> 0.0.1"\n```\n\n\n\n## Usage\n\n```ruby\nrequire "bundler/setup"\nrequire "ark_email"\n\nark = ArkEmail::Client.new(\n api_key: ENV["ARK_API_KEY"] # This is the default and can be omitted\n)\n\nresponse = ark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome"\n)\n\nputs(response.data)\n```\n\n\n\n### Pagination\n\nList methods in the Ark API are paginated.\n\nThis library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:\n\n```ruby\npage = ark.emails.list(page: 1, per_page: 10)\n\n# Fetch single item from page.\nemail = page.data[0]\nputs(email.id)\n\n# Automatically fetches more pages as needed.\npage.auto_paging_each do |email|\n puts(email.id)\nend\n```\n\nAlternatively, you can use the `#next_page?` and `#next_page` methods for more granular control working with pages.\n\n```ruby\nif page.next_page?\n new_page = page.next_page\n puts(new_page.data[0].id)\nend\n```\n\n\n\n### Handling errors\n\nWhen the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `ArkEmail::Errors::APIError` will be thrown:\n\n```ruby\nbegin\n email = ark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome"\n )\nrescue ArkEmail::Errors::APIConnectionError => e\n puts("The server could not be reached")\n puts(e.cause) # an underlying Exception, likely raised within `net/http`\nrescue ArkEmail::Errors::RateLimitError => e\n puts("A 429 status code was received; we should back off a bit.")\nrescue ArkEmail::Errors::APIStatusError => e\n puts("Another non-200-range status code was received")\n puts(e.status)\nend\n```\n\nError codes are as follows:\n\n| Cause | Error Type |\n| ---------------- | -------------------------- |\n| HTTP 400 | `BadRequestError` |\n| HTTP 401 | `AuthenticationError` |\n| HTTP 403 | `PermissionDeniedError` |\n| HTTP 404 | `NotFoundError` |\n| HTTP 409 | `ConflictError` |\n| HTTP 422 | `UnprocessableEntityError` |\n| HTTP 429 | `RateLimitError` |\n| HTTP >= 500 | `InternalServerError` |\n| Other HTTP error | `APIStatusError` |\n| Timeout | `APITimeoutError` |\n| Network error | `APIConnectionError` |\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\n\nConnection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, >=500 Internal errors, and timeouts will all be retried by default.\n\nYou can use the `max_retries` option to configure or disable this:\n\n```ruby\n# Configure the default for all requests:\nark = ArkEmail::Client.new(\n max_retries: 0 # default is 2\n)\n\n# Or, configure per-request:\nark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome",\n request_options: {max_retries: 5}\n)\n```\n\n### Timeouts\n\nBy default, requests will time out after 60 seconds. You can use the timeout option to configure or disable this:\n\n```ruby\n# Configure the default for all requests:\nark = ArkEmail::Client.new(\n timeout: nil # default is 60\n)\n\n# Or, configure per-request:\nark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome",\n request_options: {timeout: 5}\n)\n```\n\nOn timeout, `ArkEmail::Errors::APITimeoutError` is raised.\n\nNote that requests that time out are retried by default.\n\n## Advanced concepts\n\n### BaseModel\n\nAll parameter and response objects inherit from `ArkEmail::Internal::Type::BaseModel`, which provides several conveniences, including:\n\n1. All fields, including unknown ones, are accessible with `obj[:prop]` syntax, and can be destructured with `obj => {prop: prop}` or pattern-matching syntax.\n\n2. Structural equivalence for equality; if two API calls return the same values, comparing the responses with == will return true.\n\n3. Both instances and the classes themselves can be pretty-printed.\n\n4. Helpers such as `#to_h`, `#deep_to_h`, `#to_json`, and `#to_yaml`.\n\n### Making custom or undocumented requests\n\n#### Undocumented properties\n\nYou can send undocumented parameters to any endpoint, and read undocumented response properties, like so:\n\nNote: the `extra_` parameters of the same name overrides the documented parameters.\n\n```ruby\nresponse =\n ark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome",\n request_options: {\n extra_query: {my_query_parameter: value},\n extra_body: {my_body_parameter: value},\n extra_headers: {"my-header": value}\n }\n )\n\nputs(response[:my_undocumented_property])\n```\n\n#### Undocumented request params\n\nIf you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` under the `request_options:` parameter when making a request, as seen in the examples above.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints while retaining the benefit of auth, retries, and so on, you can make requests using `client.request`, like so:\n\n```ruby\nresponse = client.request(\n method: :post,\n path: \'/undocumented/endpoint\',\n query: {"dog": "woof"},\n headers: {"useful-header": "interesting-value"},\n body: {"hello": "world"}\n)\n```\n\n### Concurrency & connection pooling\n\nThe `ArkEmail::Client` instances are threadsafe, but are only are fork-safe when there are no in-flight HTTP requests.\n\nEach instance of `ArkEmail::Client` has its own HTTP connection pool with a default size of 99. As such, we recommend instantiating the client once per application in most settings.\n\nWhen all available connections from the pool are checked out, requests wait for a new connection to become available, with queue time counting towards the request timeout.\n\nUnless otherwise specified, other classes in the SDK do not have locks protecting their underlying data structure.\n\n## Sorbet\n\nThis library provides comprehensive [RBI](https://sorbet.org/docs/rbi) definitions, and has no dependency on sorbet-runtime.\n\nYou can provide typesafe request parameters like so:\n\n```ruby\nark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome"\n)\n```\n\nOr, equivalently:\n\n```ruby\n# Hashes work, but are not typesafe:\nark.emails.send_(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome"\n)\n\n# You can also splat a full Params class:\nparams = ArkEmail::EmailSendParams.new(\n from: "hello@yourdomain.com",\n subject: "Hello World",\n to: ["user@example.com"],\n html: "

Welcome!

",\n metadata: {user_id: "usr_123456", campaign: "onboarding"},\n tag: "welcome"\n)\nark.emails.send_(**params)\n```\n\n### Enums\n\nSince this library does not depend on `sorbet-runtime`, it cannot provide [`T::Enum`](https://sorbet.org/docs/tenum) instances. Instead, we provide "tagged symbols" instead, which is always a primitive at runtime:\n\n```ruby\n# :pending\nputs(ArkEmail::EmailListParams::Status::PENDING)\n\n# Revealed type: `T.all(ArkEmail::EmailListParams::Status, Symbol)`\nT.reveal_type(ArkEmail::EmailListParams::Status::PENDING)\n```\n\nEnum parameters have a "relaxed" type, so you can either pass in enum constants or their literal value:\n\n```ruby\n# Using the enum constants preserves the tagged type information:\nark.emails.list(\n status: ArkEmail::EmailListParams::Status::PENDING,\n # …\n)\n\n# Literal values are also permissible:\nark.emails.list(\n status: :pending,\n # …\n)\n```\n\n## Versioning\n\nThis package follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions. As the library is in initial development and has a major version of `0`, APIs may change at any time.\n\nThis package considers improvements to the (non-runtime) `*.rbi` and `*.rbs` type definitions to be non-breaking changes.\n\n## Requirements\n\nRuby 3.2.0 or higher.\n\n## Contributing\n\nSee [the contributing documentation](https://github.com/ArkHQ-io/ark-ruby/tree/main/CONTRIBUTING.md).\n', + }, +]; + +const INDEX_OPTIONS = { + fields: [ + 'name', + 'endpoint', + 'summary', + 'description', + 'qualified', + 'stainlessPath', + 'content', + 'sectionContext', + ], + storeFields: ['kind', '_original'], + searchOptions: { + prefix: true, + fuzzy: 0.1, + boost: { + name: 5, + stainlessPath: 3, + endpoint: 3, + qualified: 3, + summary: 2, + content: 1, + description: 1, + } as Record, + }, +}; + +/** + * Self-contained local search engine backed by MiniSearch. + * Method data is embedded at SDK build time; prose documents + * can be loaded from an optional docs directory at runtime. + */ +export class LocalDocsSearch { + private methodIndex: MiniSearch; + private proseIndex: MiniSearch; + + private constructor() { + this.methodIndex = new MiniSearch(INDEX_OPTIONS); + this.proseIndex = new MiniSearch(INDEX_OPTIONS); + } + + static async create(opts?: { docsDir?: string }): Promise { + const instance = new LocalDocsSearch(); + instance.indexMethods(EMBEDDED_METHODS); + for (const readme of EMBEDDED_READMES) { + instance.indexProse(readme.content, `readme:${readme.language}`); + } + if (opts?.docsDir) { + await instance.loadDocsDirectory(opts.docsDir); + } + return instance; + } + + search(props: { + query: string; + language?: string; + detail?: string; + maxResults?: number; + maxLength?: number; + }): SearchResult { + const { query, language = 'typescript', detail = 'default', maxResults = 5, maxLength = 100_000 } = props; + + const useMarkdown = detail === 'verbose' || detail === 'high'; + + // Search both indices and merge results by score. + // Filter prose hits so language-tagged content (READMEs and docs with + // frontmatter) only matches the requested language. + const methodHits = this.methodIndex + .search(query) + .map((hit) => ({ ...hit, _kind: 'http_method' as const })); + const proseHits = this.proseIndex + .search(query) + .filter((hit) => { + const source = ((hit as Record)['_original'] as ProseChunk | undefined)?.source; + if (!source) return true; + // Check for language-tagged sources: "readme:" or "lang::" + let taggedLang: string | undefined; + if (source.startsWith('readme:')) taggedLang = source.slice('readme:'.length); + else if (source.startsWith('lang:')) taggedLang = source.split(':')[1]; + if (!taggedLang) return true; + return taggedLang === language || (language === 'javascript' && taggedLang === 'typescript'); + }) + .map((hit) => ({ ...hit, _kind: 'prose' as const })); + const merged = [...methodHits, ...proseHits].sort((a, b) => b.score - a.score); + const top = merged.slice(0, maxResults); + + const fullResults: (string | Record)[] = []; + + for (const hit of top) { + const original = (hit as Record)['_original']; + if (hit._kind === 'http_method') { + const m = original as MethodEntry; + if (useMarkdown && m.markdown) { + fullResults.push(m.markdown); + } else { + // Use per-language data when available, falling back to the + // top-level fields (which are TypeScript-specific in the + // legacy codepath). + const langData = m.perLanguage?.[language]; + fullResults.push({ + method: langData?.method ?? m.qualified, + summary: m.summary, + description: m.description, + endpoint: `${m.httpMethod.toUpperCase()} ${m.endpoint}`, + ...(langData?.example ? { example: langData.example } : {}), + ...(m.params ? { params: m.params } : {}), + ...(m.response ? { response: m.response } : {}), + }); + } + } else { + const c = original as ProseChunk; + fullResults.push({ + content: c.content, + ...(c.source ? { source: c.source } : {}), + }); + } + } + + let totalLength = 0; + const results: (string | Record)[] = []; + for (const result of fullResults) { + const len = typeof result === 'string' ? result.length : JSON.stringify(result).length; + totalLength += len; + if (totalLength > maxLength) break; + results.push(result); + } + + if (results.length < fullResults.length) { + results.unshift(`Truncated; showing ${results.length} of ${fullResults.length} results.`); + } + + return { results }; + } + + private indexMethods(methods: MethodEntry[]): void { + const docs: MiniSearchDocument[] = methods.map((m, i) => ({ + id: `method-${i}`, + kind: 'http_method' as const, + name: m.name, + endpoint: m.endpoint, + summary: m.summary, + description: m.description, + qualified: m.qualified, + stainlessPath: m.stainlessPath, + _original: m as unknown as Record, + })); + if (docs.length > 0) { + this.methodIndex.addAll(docs); + } + } + + private async loadDocsDirectory(docsDir: string): Promise { + let entries; + try { + entries = await fs.readdir(docsDir, { withFileTypes: true }); + } catch (err) { + getLogger().warn({ err, docsDir }, 'Could not read docs directory'); + return; + } + + const files = entries + .filter((e) => e.isFile()) + .filter((e) => e.name.endsWith('.md') || e.name.endsWith('.markdown') || e.name.endsWith('.json')); + + for (const file of files) { + try { + const filePath = path.join(docsDir, file.name); + const content = await fs.readFile(filePath, 'utf-8'); + + if (file.name.endsWith('.json')) { + const texts = extractTexts(JSON.parse(content)); + if (texts.length > 0) { + this.indexProse(texts.join('\n\n'), file.name); + } + } else { + // Parse optional YAML frontmatter for language tagging. + // Files with a "language" field in frontmatter will only + // surface in searches for that language. + // + // Example: + // --- + // language: python + // --- + // # Error handling in Python + // ... + const frontmatter = parseFrontmatter(content); + const source = frontmatter.language ? `lang:${frontmatter.language}:${file.name}` : file.name; + this.indexProse(content, source); + } + } catch (err) { + getLogger().warn({ err, file: file.name }, 'Failed to index docs file'); + } + } + } + + private indexProse(markdown: string, source: string): void { + const chunks = chunkMarkdown(markdown); + const baseId = this.proseIndex.documentCount; + + const docs: MiniSearchDocument[] = chunks.map((chunk, i) => ({ + id: `prose-${baseId + i}`, + kind: 'prose' as const, + content: chunk.content, + ...(chunk.sectionContext != null ? { sectionContext: chunk.sectionContext } : {}), + _original: { ...chunk, source } as unknown as Record, + })); + + if (docs.length > 0) { + this.proseIndex.addAll(docs); + } + } +} + +/** Lightweight markdown chunker — splits on headers, chunks by word count. */ +function chunkMarkdown(markdown: string): { content: string; tag: string; sectionContext?: string }[] { + // Strip YAML frontmatter + const stripped = markdown.replace(/^---\n[\s\S]*?\n---\n?/, ''); + const lines = stripped.split('\n'); + + const chunks: { content: string; tag: string; sectionContext?: string }[] = []; + const headers: string[] = []; + let current: string[] = []; + + const flush = () => { + const text = current.join('\n').trim(); + if (!text) return; + const sectionContext = headers.length > 0 ? headers.join(' > ') : undefined; + // Split into ~200-word chunks + const words = text.split(/\s+/); + for (let i = 0; i < words.length; i += 200) { + const slice = words.slice(i, i + 200).join(' '); + if (slice) { + chunks.push({ content: slice, tag: 'p', ...(sectionContext != null ? { sectionContext } : {}) }); + } + } + current = []; + }; + + for (const line of lines) { + const headerMatch = line.match(/^(#{1,6})\s+(.+)/); + if (headerMatch) { + flush(); + const level = headerMatch[1]!.length; + const text = headerMatch[2]!.trim(); + while (headers.length >= level) headers.pop(); + headers.push(text); + } else { + current.push(line); + } + } + flush(); + + return chunks; +} + +/** Recursively extracts string values from a JSON structure. */ +function extractTexts(data: unknown, depth = 0): string[] { + if (depth > 10) return []; + if (typeof data === 'string') return data.trim() ? [data] : []; + if (Array.isArray(data)) return data.flatMap((item) => extractTexts(item, depth + 1)); + if (typeof data === 'object' && data !== null) { + return Object.values(data).flatMap((v) => extractTexts(v, depth + 1)); + } + return []; +} + +/** Parses YAML frontmatter from a markdown string, extracting the language field if present. */ +function parseFrontmatter(markdown: string): { language?: string } { + const match = markdown.match(/^---\n([\s\S]*?)\n---/); + if (!match) return {}; + const body = match[1] ?? ''; + const langMatch = body.match(/^language:\s*(.+)$/m); + return langMatch ? { language: langMatch[1]!.trim() } : {}; +} diff --git a/packages/mcp-server/src/logger.ts b/packages/mcp-server/src/logger.ts new file mode 100644 index 0000000..29dab11 --- /dev/null +++ b/packages/mcp-server/src/logger.ts @@ -0,0 +1,28 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { pino, type Level, type Logger } from 'pino'; +import pretty from 'pino-pretty'; + +let _logger: Logger | undefined; + +export function configureLogger({ level, pretty: usePretty }: { level: Level; pretty: boolean }): void { + _logger = pino( + { + level, + timestamp: pino.stdTimeFunctions.isoTime, + formatters: { + level(label) { + return { level: label }; + }, + }, + }, + usePretty ? pretty({ colorize: true, levelFirst: true, destination: 2 }) : process.stderr, + ); +} + +export function getLogger(): Logger { + if (!_logger) { + throw new Error('Logger has not been configured. Call configureLogger() before using the logger.'); + } + return _logger; +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 32a8871..f151876 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -8,19 +8,27 @@ import { readEnv } from './util'; export type CLIOptions = McpOptions & { debug: boolean; + logFormat: 'json' | 'pretty'; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; }; export type McpOptions = { + includeCodeTool?: boolean | undefined; includeDocsTools?: boolean | undefined; stainlessApiKey?: string | undefined; + docsSearchMode?: 'stainless-api' | 'local' | undefined; + docsDir?: string | undefined; codeAllowHttpGets?: boolean | undefined; codeAllowedMethods?: string[] | undefined; codeBlockedMethods?: string[] | undefined; + codeExecutionMode: McpCodeExecutionMode; + customInstructionsPath?: string | undefined; }; +export type McpCodeExecutionMode = 'stainless-sandbox' | 'local'; + export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) .option('code-allow-http-gets', { @@ -40,7 +48,35 @@ export function parseCLIOptions(): CLIOptions { description: 'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', }) + .option('code-execution-mode', { + type: 'string', + choices: ['stainless-sandbox', 'local'], + default: 'stainless-sandbox', + description: + "Where to run code execution in code tool; 'stainless-sandbox' will execute code in Stainless-hosted sandboxes whereas 'local' will execute code locally on the MCP server machine.", + }) + .option('custom-instructions-path', { + type: 'string', + description: 'Path to custom instructions for the MCP server', + }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) + .option('docs-dir', { + type: 'string', + description: + 'Path to a directory of local documentation files (markdown/JSON) to include in local docs search.', + }) + .option('docs-search-mode', { + type: 'string', + choices: ['stainless-api', 'local'], + default: 'stainless-api', + description: + "Where to search documentation; 'stainless-api' uses the Stainless-hosted search API whereas 'local' uses an in-memory search index built from embedded SDK method data and optional local docs files.", + }) + .option('log-format', { + type: 'string', + choices: ['json', 'pretty'], + description: 'Format for log output; defaults to json unless tty is detected', + }) .option('no-tools', { type: 'string', array: true, @@ -82,18 +118,29 @@ export function parseCLIOptions(): CLIOptions { : argv.tools?.includes(toolType) ? true : undefined; + const includeCodeTool = shouldIncludeToolType('code'); const includeDocsTools = shouldIncludeToolType('docs'); const transport = argv.transport as 'stdio' | 'http'; + const logFormat = + argv.logFormat ? (argv.logFormat as 'json' | 'pretty') + : process.stderr.isTTY ? 'pretty' + : 'json'; return { + ...(includeCodeTool !== undefined && { includeCodeTool }), ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, stainlessApiKey: argv.stainlessApiKey, + docsSearchMode: argv.docsSearchMode as 'stainless-api' | 'local' | undefined, + docsDir: argv.docsDir, codeAllowHttpGets: argv.codeAllowHttpGets, codeAllowedMethods: argv.codeAllowedMethods, codeBlockedMethods: argv.codeBlockedMethods, + codeExecutionMode: argv.codeExecutionMode as McpCodeExecutionMode, + customInstructionsPath: argv.customInstructionsPath, transport, + logFormat, port: argv.port, socket: argv.socket, }; @@ -118,12 +165,21 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M const queryObject = typeof query === 'string' ? qs.parse(query) : query; const queryOptions = QueryOptions.parse(queryObject); + let codeTool: boolean | undefined = + queryOptions.no_tools && queryOptions.no_tools?.includes('code') ? false + : queryOptions.tools?.includes('code') ? true + : defaultOptions.includeCodeTool; + let docsTools: boolean | undefined = queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false : queryOptions.tools?.includes('docs') ? true : defaultOptions.includeDocsTools; return { + ...(codeTool !== undefined && { includeCodeTool: codeTool }), ...(docsTools !== undefined && { includeDocsTools: docsTools }), + codeExecutionMode: defaultOptions.codeExecutionMode, + docsSearchMode: defaultOptions.docsSearchMode, + docsDir: defaultOptions.docsDir, }; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 3da37b0..555f7d1 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -11,55 +11,27 @@ import { ClientOptions } from 'ark-email'; import Ark from 'ark-email'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; +import { setLocalSearch } from './docs-search-tool'; +import { LocalDocsSearch } from './local-docs-search'; +import { getInstructions } from './instructions'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; -import { readEnv } from './util'; -async function getInstructions(stainlessApiKey: string | undefined): Promise { - // Setting the stainless API key is optional, but may be required - // to authenticate requests to the Stainless API. - const response = await fetch( - readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/ark', - { - method: 'GET', - headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, - }, - ); - - let instructions: string | undefined; - if (!response.ok) { - console.warn( - 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', - ); - - instructions = ` - This is the ark MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! - `; - } - - instructions ??= ((await response.json()) as { instructions: string }).instructions; - instructions = ` - The current time in Unix timestamps is ${Date.now()}. - - ${instructions} - `; - - return instructions; -} - -export const newMcpServer = async (stainlessApiKey: string | undefined) => +export const newMcpServer = async ({ + stainlessApiKey, + customInstructionsPath, +}: { + stainlessApiKey?: string | undefined; + customInstructionsPath?: string | undefined; +}) => new McpServer( { name: 'ark_email_api', - version: '0.19.1', + version: '0.20.0', }, { - instructions: await getInstructions(stainlessApiKey), + instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }), capabilities: { tools: {}, logging: {} }, }, ); @@ -73,6 +45,9 @@ export async function initMcpServer(params: { clientOptions?: ClientOptions; mcpOptions?: McpOptions; stainlessApiKey?: string | undefined; + upstreamClientEnvs?: Record | undefined; + mcpSessionId?: string | undefined; + mcpClientInfo?: { name: string; version: string } | undefined; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; @@ -91,14 +66,38 @@ export async function initMcpServer(params: { error: logAtLevel('error'), }; - let client = new Ark({ - logger, - ...params.clientOptions, - defaultHeaders: { - ...params.clientOptions?.defaultHeaders, - 'X-Stainless-MCP': 'true', - }, - }); + if (params.mcpOptions?.docsSearchMode === 'local') { + const docsDir = params.mcpOptions?.docsDir; + const localSearch = await LocalDocsSearch.create(docsDir ? { docsDir } : undefined); + setLocalSearch(localSearch); + } + + let _client: Ark | undefined; + let _clientError: Error | undefined; + let _logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off' | undefined; + + const getClient = (): Ark => { + if (_clientError) throw _clientError; + if (!_client) { + try { + _client = new Ark({ + logger, + ...params.clientOptions, + defaultHeaders: { + ...params.clientOptions?.defaultHeaders, + 'X-Stainless-MCP': 'true', + }, + }); + if (_logLevel) { + _client = _client.withOptions({ logLevel: _logLevel }); + } + } catch (e) { + _clientError = e instanceof Error ? e : new Error(String(e)); + throw _clientError; + } + } + return _client; + }; const providedTools = selectTools(params.mcpOptions); const toolMap = Object.fromEntries(providedTools.map((mcpTool) => [mcpTool.tool.name, mcpTool])); @@ -116,11 +115,29 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } + let client: Ark; + try { + client = getClient(); + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to initialize client: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + return executeHandler({ handler: mcpTool.handler, reqContext: { client, stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey, + upstreamClientEnvs: params.upstreamClientEnvs, + mcpSessionId: params.mcpSessionId, + mcpClientInfo: params.mcpClientInfo, }, args, }); @@ -128,24 +145,29 @@ export async function initMcpServer(params: { server.setRequestHandler(SetLevelRequestSchema, async (request) => { const { level } = request.params; + let logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off'; switch (level) { case 'debug': - client = client.withOptions({ logLevel: 'debug' }); + logLevel = 'debug'; break; case 'info': - client = client.withOptions({ logLevel: 'info' }); + logLevel = 'info'; break; case 'notice': case 'warning': - client = client.withOptions({ logLevel: 'warn' }); + logLevel = 'warn'; break; case 'error': - client = client.withOptions({ logLevel: 'error' }); + logLevel = 'error'; break; default: - client = client.withOptions({ logLevel: 'off' }); + logLevel = 'off'; break; } + _logLevel = logLevel; + if (_client) { + _client = _client.withOptions({ logLevel }); + } return {}; }); } @@ -154,11 +176,16 @@ export async function initMcpServer(params: { * Selects the tools to include in the MCP Server based on the provided options. */ export function selectTools(options?: McpOptions): McpTool[] { - const includedTools = [ - codeTool({ - blockedMethods: blockedMethodsForCodeTool(options), - }), - ]; + const includedTools = []; + + if (options?.includeCodeTool ?? true) { + includedTools.push( + codeTool({ + blockedMethods: blockedMethodsForCodeTool(options), + codeExecutionMode: options?.codeExecutionMode ?? 'stainless-sandbox', + }), + ); + } if (options?.includeDocsTools ?? true) { includedTools.push(docsSearchTool); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index ceccaed..b04a544 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,13 +1,17 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; +import { getLogger } from './logger'; export const launchStdioServer = async (mcpOptions: McpOptions) => { - const server = await newMcpServer(mcpOptions.stainlessApiKey); + const server = await newMcpServer({ + stainlessApiKey: mcpOptions.stainlessApiKey, + customInstructionsPath: mcpOptions.customInstructionsPath, + }); await initMcpServer({ server, mcpOptions, stainlessApiKey: mcpOptions.stainlessApiKey }); const transport = new StdioServerTransport(); await server.connect(transport); - console.error('MCP Server running on stdio'); + getLogger().info('MCP Server running on stdio'); }; diff --git a/packages/mcp-server/src/types.ts b/packages/mcp-server/src/types.ts index fe3d649..f9f0d1b 100644 --- a/packages/mcp-server/src/types.ts +++ b/packages/mcp-server/src/types.ts @@ -45,6 +45,9 @@ export type ToolCallResult = { export type McpRequestContext = { client: Ark; stainlessApiKey?: string | undefined; + upstreamClientEnvs?: Record | undefined; + mcpSessionId?: string | undefined; + mcpClientInfo?: { name: string; version: string } | undefined; }; export type HandlerFunction = ({ diff --git a/packages/mcp-server/src/util.ts b/packages/mcp-server/src/util.ts index 40ed550..069a2b4 100644 --- a/packages/mcp-server/src/util.ts +++ b/packages/mcp-server/src/util.ts @@ -2,9 +2,9 @@ export const readEnv = (env: string): string | undefined => { if (typeof (globalThis as any).process !== 'undefined') { - return (globalThis as any).process.env?.[env]?.trim(); + return (globalThis as any).process.env?.[env]?.trim() || undefined; } else if (typeof (globalThis as any).Deno !== 'undefined') { - return (globalThis as any).Deno.env?.get?.(env)?.trim(); + return (globalThis as any).Deno.env?.get?.(env)?.trim() || undefined; } return; }; diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 7a2d511..1730629 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -1,4 +1,4 @@ -import { parseCLIOptions, parseQueryOptions } from '../src/options'; +import { parseCLIOptions } from '../src/options'; // Mock process.argv const mockArgv = (args: string[]) => { @@ -30,21 +30,3 @@ describe('parseCLIOptions', () => { cleanup(); }); }); - -describe('parseQueryOptions', () => { - const defaultOptions = {}; - - it('default parsing should be empty', () => { - const query = ''; - const result = parseQueryOptions(defaultOptions, query); - - expect(result).toEqual({}); - }); - - it('should handle invalid query string gracefully', () => { - const query = 'invalid=value&tools=invalid-operation'; - - // Should throw due to Zod validation for invalid tools - expect(() => parseQueryOptions(defaultOptions, query)).toThrow(); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0162b55..3c68588 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + minimatch: ^9.0.5 + importers: .: @@ -77,12 +80,18 @@ importers: '@cloudflare/cabidela': specifier: ^0.2.4 version: 0.2.4 + '@hono/node-server': + specifier: ^1.19.10 + version: 1.19.11(hono@4.12.5) '@modelcontextprotocol/sdk': - specifier: ^1.25.2 - version: 1.25.2(hono@4.11.4)(zod@3.25.76) + specifier: ^1.27.1 + version: 1.27.1(zod@3.25.76) '@valtown/deno-http-worker': specifier: ^0.0.21 version: 0.0.21 + ajv: + specifier: ^8.18.0 + version: 8.18.0 ark-email: specifier: workspace:* version: link:../.. @@ -98,15 +107,24 @@ importers: fuse.js: specifier: ^7.1.0 version: 7.1.0 + hono: + specifier: ^4.12.4 + version: 4.12.5 jq-web: specifier: https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz version: https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz - morgan: - specifier: ^1.10.0 - version: 1.10.1 - morgan-body: - specifier: ^2.6.9 - version: 2.6.9 + minisearch: + specifier: ^7.2.0 + version: 7.2.0 + pino: + specifier: ^10.3.1 + version: 10.3.1 + pino-http: + specifier: ^11.0.0 + version: 11.0.0 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 qs: specifier: ^6.14.1 version: 6.14.1 @@ -141,9 +159,6 @@ importers: '@types/jest': specifier: ^29.4.0 version: 29.5.11 - '@types/morgan': - specifier: ^1.9.10 - version: 1.9.10 '@types/qs': specifier: ^6.14.0 version: 6.14.0 @@ -152,19 +167,19 @@ importers: version: 17.0.32 '@typescript-eslint/eslint-plugin': specifier: 8.31.1 - version: 8.31.1(@typescript-eslint/parser@8.31.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + version: 8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1)(typescript@5.8.3) '@typescript-eslint/parser': specifier: 8.31.1 - version: 8.31.1(eslint@8.57.1)(typescript@5.8.3) + version: 8.31.1(eslint@9.39.1)(typescript@5.8.3) eslint: - specifier: ^8.49.0 - version: 8.57.1 + specifier: ^9.39.1 + version: 9.39.1 eslint-plugin-prettier: - specifier: ^5.0.1 - version: 5.4.1(eslint@8.57.1)(prettier@3.1.1) + specifier: ^5.4.1 + version: 5.4.1(eslint@9.39.1)(prettier@3.1.1) eslint-plugin-unused-imports: - specifier: ^3.0.0 - version: 3.2.0(@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1) + specifier: ^4.1.4 + version: 4.1.4(@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1) jest: specifier: ^29.4.0 version: 29.7.0(@types/node@22.19.1)(ts-node@10.7.0(@swc/core@1.4.16)(@types/node@22.19.1)(typescript@5.8.3)) @@ -417,18 +432,10 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/eslintrc@3.3.3': resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/js@9.39.1': resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -441,8 +448,8 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -455,19 +462,10 @@ packages: resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -628,8 +626,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@modelcontextprotocol/sdk@1.25.2': - resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -650,6 +648,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgr/core@0.2.4': resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -826,9 +827,6 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/morgan@1.9.10': - resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==} - '@types/mute-stream@0.0.4': resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} @@ -909,9 +907,6 @@ packages: resolution: {integrity: sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@valtown/deno-http-worker@0.0.21': resolution: {integrity: sha512-16kFuUykann75lNytnXXIQlmpzreZjzdyT27ebT3yNGCS3kKaS1iZYWHc3Si9An54Cphwr4qEcviChQkEeJBlA==} engines: {node: 20 || 22 || 24} @@ -954,8 +949,8 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -997,6 +992,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1029,17 +1028,10 @@ packages: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true - basic-auth@2.0.1: - resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} - engines: {node: '>= 0.8'} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -1152,6 +1144,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -1160,9 +1155,6 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -1205,17 +1197,8 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -1266,10 +1249,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1294,6 +1273,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1313,17 +1295,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - - es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - - es6-symbol@3.1.4: - resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} - engines: {node: '>=0.12'} - escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -1357,16 +1328,6 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-unused-imports@3.2.0: - resolution: {integrity: sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': 6 - 7 - eslint: '8' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - eslint-plugin-unused-imports@4.1.4: resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} peerDependencies: @@ -1376,14 +1337,6 @@ packages: '@typescript-eslint/eslint-plugin': optional: true - eslint-rule-composer@0.3.0: - resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} - engines: {node: '>=4.0.0'} - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1400,12 +1353,6 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1416,18 +1363,10 @@ packages: jiti: optional: true - esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1453,9 +1392,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -1476,8 +1412,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -1486,13 +1422,13 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-copy@4.0.2: + resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1509,6 +1445,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -1521,10 +1460,6 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1545,10 +1480,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -1636,10 +1567,6 @@ packages: engines: {node: '>=12'} deprecated: Glob versions prior to v9 are no longer supported - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1675,11 +1602,14 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.11.4: - resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} engines: {node: '>=16.9.0'} html-escaper@2.0.2: @@ -1741,6 +1671,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1771,10 +1705,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1941,6 +1871,10 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + jq-web@https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz: resolution: {integrity: sha512-6nmDSHGJm8Cclf2oSuO9xQ7vt5x5bDiuTKX2rkyW1K7Uo2SHeI4ZvpgMITKy+8p3bbEnKovTPGoHhApa6v/PbA==, tarball: https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz} version: 0.8.8 @@ -2084,17 +2018,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} - engines: {node: '>=10'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2102,31 +2025,18 @@ packages: minimist@1.2.6: resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + mkdirp@2.1.6: resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} engines: {node: '>=10'} hasBin: true - moment-timezone@0.5.48: - resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} - - moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - - morgan-body@2.6.9: - resolution: {integrity: sha512-O0dlv/V67gSszFo4rraZ7nMhD+iuWOg2w7hOLcAC6S+nF26Q8XvikGFjoJ7+dVuh49BBsIvQqRUGbs9e8keFKw==} - - morgan@1.10.1: - resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} - engines: {node: '>= 0.8.0'} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2147,9 +2057,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - node-emoji@2.1.3: resolution: {integrity: sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==} engines: {node: '>=18'} @@ -2193,18 +2100,14 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - on-headers@1.1.0: - resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2297,6 +2200,23 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -2330,6 +2250,9 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2343,6 +2266,9 @@ packages: engines: {node: '>=16'} hasBin: true + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2357,6 +2283,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2372,6 +2301,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2404,11 +2337,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -2420,15 +2348,19 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2496,6 +2428,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -2503,6 +2438,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2552,6 +2491,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + superstruct@1.0.4: resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} engines: {node: '>=14.0.0'} @@ -2580,9 +2523,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2590,6 +2530,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -2678,10 +2622,6 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -2694,9 +2634,6 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - type@2.7.3: - resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typescript-eslint@8.31.1: resolution: {integrity: sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2836,6 +2773,11 @@ packages: peerDependencies: zod: ^3.25 || ^4 + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.1: resolution: {integrity: sha512-F3rdaCOHs5ViJ5YTz5zzRtfkQdMdIeKudJAoxy7yB/2ZMEHw73lmCAcQw11r7++20MyGl4WV59EVh7A9rNAyog==} engines: {node: '>=18.0.0'} @@ -3083,21 +3025,11 @@ snapshots: dependencies: '@cspotcode/source-map-consumer': 0.8.0 - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.4.0(eslint@9.39.1)': dependencies: eslint: 9.39.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': dependencies: eslint: 9.39.1 @@ -3111,7 +3043,7 @@ snapshots: dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 9.0.5 transitivePeerDependencies: - supports-color @@ -3123,20 +3055,6 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 @@ -3146,13 +3064,11 @@ snapshots: ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 9.0.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} - '@eslint/js@9.39.1': {} '@eslint/object-schema@2.1.7': {} @@ -3162,9 +3078,9 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@hono/node-server@1.19.9(hono@4.11.4)': + '@hono/node-server@1.19.11(hono@4.12.5)': dependencies: - hono: 4.11.4 + hono: 4.12.5 '@humanfs/core@0.19.1': {} @@ -3173,18 +3089,8 @@ snapshots: '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.4.3 - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} - '@humanwhocodes/retry@0.4.3': {} '@inquirer/checkbox@3.0.1': @@ -3520,26 +3426,26 @@ snapshots: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - '@modelcontextprotocol/sdk@1.25.2(hono@4.11.4)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.4) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.5 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.1 zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - - hono - supports-color '@nodelib/fs.scandir@2.1.5': @@ -3554,6 +3460,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@pinojs/redact@0.4.0': {} + '@pkgr/core@0.2.4': {} '@sinclair/typebox@0.27.8': {} @@ -3630,7 +3538,7 @@ snapshots: '@ts-morph/common@0.20.0': dependencies: fast-glob: 3.3.2 - minimatch: 7.4.6 + minimatch: 9.0.5 mkdirp: 2.1.6 path-browserify: 1.0.1 @@ -3720,10 +3628,6 @@ snapshots: '@types/mime@1.3.5': {} - '@types/morgan@1.9.10': - dependencies: - '@types/node': 20.19.11 - '@types/mute-stream@0.0.4': dependencies: '@types/node': 20.19.11 @@ -3761,23 +3665,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.31.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.31.1 - '@typescript-eslint/type-utils': 8.31.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 8.31.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.31.1 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3795,18 +3682,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.31.1(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.31.1 - '@typescript-eslint/types': 8.31.1 - '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.31.1 - debug: 4.4.1 - eslint: 8.57.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.31.1 @@ -3824,17 +3699,6 @@ snapshots: '@typescript-eslint/types': 8.31.1 '@typescript-eslint/visitor-keys': 8.31.1 - '@typescript-eslint/type-utils@8.31.1(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.31.1(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 2.0.1(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.31.1(eslint@9.39.1)(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) @@ -3862,17 +3726,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.31.1(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.31.1 - '@typescript-eslint/types': 8.31.1 - '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) - eslint: 8.57.1 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.31.1(eslint@9.39.1)(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.39.1) @@ -3889,8 +3742,6 @@ snapshots: '@typescript-eslint/types': 8.31.1 eslint-visitor-keys: 4.2.0 - '@ungap/structured-clone@1.3.0': {} - '@valtown/deno-http-worker@0.0.21': {} accepts@2.0.0: @@ -3913,9 +3764,9 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-formats@3.0.1(ajv@8.17.1): + ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv@6.12.6: dependencies: @@ -3924,7 +3775,7 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -3964,6 +3815,8 @@ snapshots: argparse@2.0.1: {} + atomic-sleep@1.0.0: {} + babel-jest@29.7.0(@babel/core@7.28.6): dependencies: '@babel/core': 7.28.6 @@ -4023,10 +3876,6 @@ snapshots: baseline-browser-mapping@2.9.14: {} - basic-auth@2.0.1: - dependencies: - safe-buffer: 5.1.2 - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -4041,11 +3890,6 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -4150,12 +3994,12 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + commander@10.0.1: {} commander@13.1.0: {} - concat-map@0.0.1: {} - content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -4218,14 +4062,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - d@1.0.2: - dependencies: - es5-ext: 0.10.64 - type: 2.7.3 - - debug@2.6.9: - dependencies: - ms: 2.0.0 + dateformat@4.6.3: {} debug@4.4.1: dependencies: @@ -4249,10 +4086,6 @@ snapshots: diff@4.0.2: {} - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4271,6 +4104,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + environment@1.1.0: {} error-ex@1.3.2: @@ -4285,24 +4122,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es5-ext@0.10.64: - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esniff: 2.0.1 - next-tick: 1.1.0 - - es6-iterator@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-symbol: 3.1.4 - - es6-symbol@3.1.4: - dependencies: - d: 1.0.2 - ext: 1.7.0 - escalade@3.1.1: {} escalade@3.2.0: {} @@ -4313,13 +4132,6 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.4.1(eslint@8.57.1)(prettier@3.1.1): - dependencies: - eslint: 8.57.1 - prettier: 3.1.1 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.8 - eslint-plugin-prettier@5.4.1(eslint@9.39.1)(prettier@3.1.1): dependencies: eslint: 9.39.1 @@ -4327,26 +4139,12 @@ snapshots: prettier-linter-helpers: 1.0.0 synckit: 0.11.8 - eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-rule-composer: 0.3.0 - optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.31.1(@typescript-eslint/parser@8.31.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1): dependencies: eslint: 9.39.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1)(typescript@5.8.3) - eslint-rule-composer@0.3.0: {} - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -4358,49 +4156,6 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.2 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - eslint@9.39.1: dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) @@ -4434,31 +4189,18 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 9.0.5 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: - supports-color - esniff@2.0.1: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - event-emitter: 0.3.5 - type: 2.7.3 - espree@10.4.0: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 - espree@9.6.1: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 - esprima@4.0.1: {} esquery@1.6.0: @@ -4475,11 +4217,6 @@ snapshots: etag@1.8.1: {} - event-emitter@0.3.5: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - eventsource-parser@3.0.6: {} eventsource@3.0.7: @@ -4508,9 +4245,10 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express-rate-limit@7.5.1(express@5.2.1): + express-rate-limit@8.3.1(express@5.2.1): dependencies: express: 5.2.1 + ip-address: 10.1.0 express@5.2.1: dependencies: @@ -4545,16 +4283,14 @@ snapshots: transitivePeerDependencies: - supports-color - ext@1.7.0: - dependencies: - type: 2.7.3 - external-editor@3.1.0: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 + fast-copy@4.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -4571,6 +4307,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} fastq@1.17.1: @@ -4583,10 +4321,6 @@ snapshots: fflate@0.8.2: {} - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4616,12 +4350,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - rimraf: 3.0.2 - flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -4704,7 +4432,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 9.0.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -4713,13 +4441,9 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 5.1.6 + minimatch: 9.0.5 once: 1.4.0 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - globals@14.0.0: {} gopd@1.2.0: {} @@ -4749,9 +4473,11 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + highlight.js@10.7.3: {} - hono@4.11.4: {} + hono@4.12.5: {} html-escaper@2.0.2: {} @@ -4779,7 +4505,7 @@ snapshots: ignore-walk@5.0.1: dependencies: - minimatch: 5.1.6 + minimatch: 9.0.5 ignore@5.3.2: {} @@ -4806,6 +4532,8 @@ snapshots: inherits@2.0.4: {} + ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -4826,8 +4554,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-promise@4.0.0: {} is-stream@2.0.1: {} @@ -5279,6 +5005,8 @@ snapshots: jose@6.1.3: {} + joycon@3.1.1: {} + jq-web@https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz: {} js-tokens@4.0.0: {} @@ -5395,53 +5123,18 @@ snapshots: mimic-fn@2.1.0: {} - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - - minimatch@7.4.6: - dependencies: - brace-expansion: 2.0.2 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 minimist@1.2.6: {} - mkdirp@2.1.6: {} - - moment-timezone@0.5.48: - dependencies: - moment: 2.30.1 - - moment@2.30.1: {} - - morgan-body@2.6.9: - dependencies: - es6-symbol: 3.1.4 - moment-timezone: 0.5.48 - on-finished: 2.4.1 - on-headers: 1.1.0 + minisearch@7.2.0: {} - morgan@1.10.1: - dependencies: - basic-auth: 2.0.1 - debug: 2.6.9 - depd: 2.0.0 - on-finished: 2.3.0 - on-headers: 1.1.0 - transitivePeerDependencies: - - supports-color + mkdirp@2.1.6: {} mri@1.2.0: {} - ms@2.0.0: {} - ms@2.1.3: {} mute-stream@1.0.0: {} @@ -5458,8 +5151,6 @@ snapshots: neo-async@2.6.2: {} - next-tick@1.1.0: {} - node-emoji@2.1.3: dependencies: '@sindresorhus/is': 4.6.0 @@ -5496,16 +5187,12 @@ snapshots: object-inspect@1.13.4: {} - on-finished@2.3.0: - dependencies: - ee-first: 1.1.1 + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: dependencies: ee-first: 1.1.1 - on-headers@1.1.0: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -5588,6 +5275,49 @@ snapshots: picomatch@2.3.1: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-http@11.0.0: + dependencies: + get-caller-file: 2.0.5 + pino: 10.3.1 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.6 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pirates@4.0.6: {} pkce-challenge@5.0.0: {} @@ -5612,6 +5342,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 + process-warning@5.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5628,6 +5360,11 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@6.0.4: {} @@ -5638,6 +5375,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + range-parser@1.2.1: {} raw-body@3.0.1: @@ -5655,6 +5394,8 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + real-require@0.2.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -5677,10 +5418,6 @@ snapshots: reusify@1.0.4: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - router@2.2.0: dependencies: debug: 4.4.3 @@ -5699,12 +5436,14 @@ snapshots: dependencies: mri: 1.2.0 - safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.1: {} @@ -5784,6 +5523,10 @@ snapshots: slash@3.0.0: {} + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -5791,6 +5534,8 @@ snapshots: source-map@0.6.1: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} stack-utils@2.0.6: @@ -5832,6 +5577,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + superstruct@1.0.4: {} supports-color@7.2.0: @@ -5857,9 +5604,7 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 - minimatch: 3.1.2 - - text-table@0.2.0: {} + minimatch: 9.0.5 thenify-all@1.6.0: dependencies: @@ -5869,6 +5614,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -6000,8 +5749,6 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.20.2: {} - type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -6012,8 +5759,6 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 - type@2.7.3: {} - typescript-eslint@8.31.1(eslint@9.39.1)(typescript@5.8.3): dependencies: '@typescript-eslint/eslint-plugin': 8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.39.1)(typescript@5.8.3))(eslint@9.39.1)(typescript@5.8.3) @@ -6136,6 +5881,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@4.0.1(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/release-please-config.json b/release-please-config.json index c45c17b..363e246 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -67,6 +67,11 @@ "type": "json", "path": "packages/mcp-server/package.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/mcp-server/manifest.json", + "jsonpath": "$.version" } ] } diff --git a/scripts/mock b/scripts/mock index 0b28f6e..5cd7c15 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,23 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stdy/cli@0.20.2 -- steady --version - # Wait for server to come online + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=0 + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Steady server to start" + cat .stdy.log + exit 1 + fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 7bce051..a9d718c 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi diff --git a/src/client.ts b/src/client.ts index 96080a0..a5728c2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,6 +11,7 @@ import type { APIResponseProps } from './internal/parse'; import { getPlatformHeaders } from './internal/detect-platform'; import * as Shims from './internal/shims'; import * as Opts from './internal/request-options'; +import { stringifyQuery } from './internal/utils/query'; import { VERSION } from './version'; import * as Errors from './core/error'; import * as Pagination from './core/pagination'; @@ -274,21 +275,8 @@ export class Ark { /** * Basic re-implementation of `qs.stringify` for primitive types. */ - protected stringifyQuery(query: Record): string { - return Object.entries(query) - .filter(([_, value]) => typeof value !== 'undefined') - .map(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } - if (value === null) { - return `${encodeURIComponent(key)}=`; - } - throw new Errors.ArkError( - `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, - ); - }) - .join('&'); + protected stringifyQuery(query: object | Record): string { + return stringifyQuery(query); } private getUserAgent(): string { @@ -320,12 +308,13 @@ export class Ark { : new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); const defaultQuery = this.defaultQuery(); - if (!isEmptyObj(defaultQuery)) { - query = { ...defaultQuery, ...query }; + const pathQuery = Object.fromEntries(url.searchParams); + if (!isEmptyObj(defaultQuery) || !isEmptyObj(pathQuery)) { + query = { ...pathQuery, ...defaultQuery, ...query }; } if (typeof query === 'object' && query && !Array.isArray(query)) { - url.search = this.stringifyQuery(query as Record); + url.search = this.stringifyQuery(query); } return url.toString(); @@ -654,9 +643,9 @@ export class Ark { } } - // If the API asks us to wait a certain amount of time (and it's a reasonable amount), - // just do what it says, but otherwise calculate a default - if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) { + // If the API asks us to wait a certain amount of time, just do what it + // says, but otherwise calculate a default + if (timeoutMillis === undefined) { const maxRetries = options.maxRetries ?? this.maxRetries; timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries); } @@ -788,7 +777,7 @@ export class Ark { ) { return { bodyHeaders: { 'content-type': 'application/x-www-form-urlencoded' }, - body: this.stringifyQuery(body as Record), + body: this.stringifyQuery(body), }; } else { return this.#encoder({ body, headers }); @@ -814,10 +803,93 @@ export class Ark { static toFile = Uploads.toFile; + /** + * Send and manage email messages. + * + * **Quick Reference:** + * - `POST /emails` - Send a single email + * - `POST /emails/batch` - Send up to 100 emails + * - `GET /emails/{emailId}` - Get email status and details + * - `GET /emails` - List sent emails + * - `POST /emails/{emailId}/retry` - Retry failed delivery + * + */ emails: API.Emails = new API.Emails(this); + /** + * Access API request logs for debugging and monitoring. + * + * Every API request is logged with details including: + * - Request method, path, and endpoint + * - Response status code and duration + * - Error details (code, message) for failed requests + * - SDK information (name, version) + * - Rate limit state at time of request + * - Request and response bodies (for single log retrieval) + * + * **Retention:** Logs are retained for 90 days. + * + * **Body storage:** Request and response bodies are stored encrypted + * and truncated at 25KB. Bodies are only returned when retrieving + * a single log entry. + * + * **Quick Reference:** + * - `GET /logs` - List API request logs with filters + * - `GET /logs/{requestId}` - Get full details including request/response bodies + * + */ logs: API.Logs = new API.Logs(this); + /** + * Per-tenant usage analytics and bulk reporting. + * + * Track email sending statistics for each tenant to power billing, dashboards, and monitoring. + * + * **Single Tenant Usage:** + * - `GET /tenants/{id}/usage` - Get usage stats for a specific tenant + * - `GET /tenants/{id}/usage/timeseries` - Get time-bucketed data for charts + * + * **Bulk Usage:** + * - `GET /usage/tenants` - Get usage for all tenants (paginated, sortable) + * - `GET /usage/export` - Export usage data as CSV, JSONL, or JSON + * + * **Period Formats:** + * - Shortcuts: `today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days` + * - Month: `2024-01` + * - Date range: `2024-01-01..2024-01-15` + * + */ usage: API.Usage = new API.Usage(this); + /** + * Check account rate limits and send limits. + * + * The limits endpoint returns current status for operational limits: + * - **Rate limit:** API requests per second (currently 10/sec) + * - **Send limit:** Emails per hour (default 100/hour for new accounts) + * - **Billing:** Credit balance and auto-recharge configuration + * + * **AI Integration Note:** This endpoint is designed for AI agents and MCP servers + * to understand account constraints before taking actions. Call this endpoint + * first when planning batch operations to avoid hitting limits unexpectedly. + * + * **Quick Reference:** + * - `GET /limits` - Get current rate limits and send limits + * - `GET /usage` - (Deprecated) Use `/limits` instead + * + */ limits: API.Limits = new API.Limits(this); + /** + * Manage tenants (your customers). + * + * Create a tenant for each of your customers to track their email sending separately. + * Store the tenant `id` in your database and use `metadata` for any custom data. + * + * **Quick Reference:** + * - `POST /tenants` - Create a new tenant + * - `GET /tenants` - List all tenants (paginated) + * - `GET /tenants/{id}` - Get tenant details + * - `PATCH /tenants/{id}` - Update tenant name, metadata, or status + * - `DELETE /tenants/{id}` - Delete a tenant + * + */ tenants: API.Tenants = new API.Tenants(this); platform: API.Platform = new API.Platform(this); } diff --git a/src/internal/utils.ts b/src/internal/utils.ts index 3cbfacc..c591353 100644 --- a/src/internal/utils.ts +++ b/src/internal/utils.ts @@ -6,3 +6,4 @@ export * from './utils/env'; export * from './utils/log'; export * from './utils/uuid'; export * from './utils/sleep'; +export * from './utils/query'; diff --git a/src/internal/utils/env.ts b/src/internal/utils/env.ts index 2d84800..cc5fa0f 100644 --- a/src/internal/utils/env.ts +++ b/src/internal/utils/env.ts @@ -9,10 +9,10 @@ */ export const readEnv = (env: string): string | undefined => { if (typeof (globalThis as any).process !== 'undefined') { - return (globalThis as any).process.env?.[env]?.trim() ?? undefined; + return (globalThis as any).process.env?.[env]?.trim() || undefined; } if (typeof (globalThis as any).Deno !== 'undefined') { - return (globalThis as any).Deno.env?.get?.(env)?.trim(); + return (globalThis as any).Deno.env?.get?.(env)?.trim() || undefined; } return undefined; }; diff --git a/src/internal/utils/query.ts b/src/internal/utils/query.ts new file mode 100644 index 0000000..4750099 --- /dev/null +++ b/src/internal/utils/query.ts @@ -0,0 +1,23 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { ArkError } from '../../core/error'; + +/** + * Basic re-implementation of `qs.stringify` for primitive types. + */ +export function stringifyQuery(query: object | Record) { + return Object.entries(query) + .filter(([_, value]) => typeof value !== 'undefined') + .map(([key, value]) => { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + if (value === null) { + return `${encodeURIComponent(key)}=`; + } + throw new ArkError( + `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, + ); + }) + .join('&'); +} diff --git a/src/resources/emails.ts b/src/resources/emails.ts index acc1201..f75ae37 100644 --- a/src/resources/emails.ts +++ b/src/resources/emails.ts @@ -8,6 +8,16 @@ import { buildHeaders } from '../internal/headers'; import { RequestOptions } from '../internal/request-options'; import { path } from '../internal/utils/path'; +/** + * Send and manage email messages. + * + * **Quick Reference:** + * - `POST /emails` - Send a single email + * - `POST /emails/batch` - Send up to 100 emails + * - `GET /emails/{emailId}` - Get email status and details + * - `GET /emails` - List sent emails + * - `POST /emails/{emailId}/retry` - Retry failed delivery + */ export class Emails extends APIResource { /** * Retrieve detailed information about a specific email including delivery status, @@ -143,6 +153,7 @@ export class Emails extends APIResource { * to: ['user@example.com'], * html: '

Welcome!

Thanks for signing up.

', * metadata: { user_id: 'usr_123', campaign: 'onboarding' }, + * tenantId: 'cm6abc123def456', * }); * ``` */ @@ -185,6 +196,7 @@ export class Emails extends APIResource { * }, * ], * from: 'notifications@myapp.com', + * tenantId: 'cm6abc123def456', * }); * ``` */ @@ -266,6 +278,11 @@ export namespace EmailRetrieveResponse { */ subject: string; + /** + * The tenant ID this email belongs to + */ + tenantId: string; + /** * Unix timestamp when the email was sent */ @@ -547,6 +564,11 @@ export interface EmailListResponse { subject: string; + /** + * The tenant ID this email belongs to + */ + tenantId: string; + timestamp: number; timestampIso: string; @@ -601,6 +623,11 @@ export namespace EmailRetrieveDeliveriesResponse { * - `bounced` - Bounced by recipient server */ status: 'pending' | 'sent' | 'softfail' | 'hardfail' | 'held' | 'bounced'; + + /** + * The tenant ID this email belongs to + */ + tenantId: string; } export namespace Data { @@ -758,6 +785,11 @@ export namespace EmailRetryResponse { id: string; message: string; + + /** + * The tenant ID this email belongs to + */ + tenantId: string; } } @@ -781,6 +813,11 @@ export namespace EmailSendResponse { */ status: 'pending' | 'sent'; + /** + * The tenant ID this email was sent from + */ + tenantId: string; + /** * List of recipient addresses */ @@ -824,6 +861,11 @@ export namespace EmailSendBatchResponse { */ messages: { [key: string]: Data.Messages }; + /** + * The tenant ID this batch was sent from + */ + tenantId: string; + /** * Total emails in the batch */ @@ -866,6 +908,11 @@ export namespace EmailSendRawResponse { */ status: 'pending' | 'sent'; + /** + * The tenant ID this email was sent from + */ + tenantId: string; + /** * List of recipient addresses */ @@ -1025,6 +1072,17 @@ export interface EmailSendParams { */ tag?: string | null; + /** + * Body param: The tenant ID to send this email from. Determines which tenant's + * configuration (domains, webhooks, tracking) is used. + * + * - If your API key is scoped to a specific tenant, this must match that tenant or + * be omitted. + * - If your API key is org-level, specify the tenant to send from. + * - If omitted, the organization's default tenant is used. + */ + tenantId?: string | null; + /** * Body param: Plain text body (accepts null, auto-generated from HTML if not * provided). Maximum 5MB (5,242,880 characters). @@ -1068,6 +1126,17 @@ export interface EmailSendBatchParams { */ from: string; + /** + * Body param: The tenant ID to send this batch from. Determines which tenant's + * configuration (domains, webhooks, tracking) is used. + * + * - If your API key is scoped to a specific tenant, this must match that tenant or + * be omitted. + * - If your API key is org-level, specify the tenant to send from. + * - If omitted, the organization's default tenant is used. + */ + tenantId?: string | null; + /** * Header param: Unique key for idempotent requests. If a request with this key was * already processed, the cached response is returned. Keys expire after 24 hours. @@ -1146,6 +1215,17 @@ export interface EmailSendRawParams { * Whether this is a bounce message (accepts null) */ bounce?: boolean | null; + + /** + * The tenant ID to send this email from. Determines which tenant's configuration + * (domains, webhooks, tracking) is used. + * + * - If your API key is scoped to a specific tenant, this must match that tenant or + * be omitted. + * - If your API key is org-level, specify the tenant to send from. + * - If omitted, the organization's default tenant is used. + */ + tenantId?: string | null; } export declare namespace Emails { diff --git a/src/resources/limits.ts b/src/resources/limits.ts index aff3185..7033db6 100644 --- a/src/resources/limits.ts +++ b/src/resources/limits.ts @@ -5,6 +5,22 @@ import * as Shared from './shared'; import { APIPromise } from '../core/api-promise'; import { RequestOptions } from '../internal/request-options'; +/** + * Check account rate limits and send limits. + * + * The limits endpoint returns current status for operational limits: + * - **Rate limit:** API requests per second (currently 10/sec) + * - **Send limit:** Emails per hour (default 100/hour for new accounts) + * - **Billing:** Credit balance and auto-recharge configuration + * + * **AI Integration Note:** This endpoint is designed for AI agents and MCP servers + * to understand account constraints before taking actions. Call this endpoint + * first when planning batch operations to avoid hitting limits unexpectedly. + * + * **Quick Reference:** + * - `GET /limits` - Get current rate limits and send limits + * - `GET /usage` - (Deprecated) Use `/limits` instead + */ export class Limits extends APIResource { /** * Returns current rate limit and send limit information for your account. diff --git a/src/resources/logs.ts b/src/resources/logs.ts index 77e5f46..c4a65cd 100644 --- a/src/resources/logs.ts +++ b/src/resources/logs.ts @@ -7,6 +7,27 @@ import { PageNumberPagination, type PageNumberPaginationParams, PagePromise } fr import { RequestOptions } from '../internal/request-options'; import { path } from '../internal/utils/path'; +/** + * Access API request logs for debugging and monitoring. + * + * Every API request is logged with details including: + * - Request method, path, and endpoint + * - Response status code and duration + * - Error details (code, message) for failed requests + * - SDK information (name, version) + * - Rate limit state at time of request + * - Request and response bodies (for single log retrieval) + * + * **Retention:** Logs are retained for 90 days. + * + * **Body storage:** Request and response bodies are stored encrypted + * and truncated at 25KB. Bodies are only returned when retrieving + * a single log entry. + * + * **Quick Reference:** + * - `GET /logs` - List API request logs with filters + * - `GET /logs/{requestId}` - Get full details including request/response bodies + */ export class Logs extends APIResource { /** * Retrieve detailed information about a specific API request log, including the diff --git a/src/resources/tenants/domains.ts b/src/resources/tenants/domains.ts index 60cd851..97af554 100644 --- a/src/resources/tenants/domains.ts +++ b/src/resources/tenants/domains.ts @@ -7,6 +7,20 @@ import { APIPromise } from '../../core/api-promise'; import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +/** + * Manage sending domains. + * + * Before you can send emails, you need to: + * 1. Add a domain + * 2. Configure DNS records (SPF, DKIM, Return Path) + * 3. Verify the domain + * + * **Quick Reference:** + * - `POST /domains` - Add a new domain + * - `GET /domains` - List all domains + * - `POST /domains/{id}/verify` - Check DNS and verify domain + * - `DELETE /domains/{id}` - Remove a domain + */ export class Domains extends APIResource { /** * Add a new sending domain to a tenant. Returns DNS records that must be diff --git a/src/resources/tenants/suppressions.ts b/src/resources/tenants/suppressions.ts index 6449e1f..de7170e 100644 --- a/src/resources/tenants/suppressions.ts +++ b/src/resources/tenants/suppressions.ts @@ -7,6 +7,18 @@ import { PageNumberPagination, type PageNumberPaginationParams, PagePromise } fr import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +/** + * Manage the suppression list. + * + * Suppressed email addresses will not receive any emails. Addresses are + * automatically suppressed when they hard bounce or file spam complaints. + * + * **Quick Reference:** + * - `GET /suppressions` - List suppressed addresses + * - `POST /suppressions` - Add to suppression list + * - `DELETE /suppressions/{email}` - Remove from suppression list + * - `GET /suppressions/{email}` - Check if address is suppressed + */ export class Suppressions extends APIResource { /** * Add an email address to the tenant's suppression list. The address will not diff --git a/src/resources/tenants/tenants.ts b/src/resources/tenants/tenants.ts index bb5f888..159b9ec 100644 --- a/src/resources/tenants/tenants.ts +++ b/src/resources/tenants/tenants.ts @@ -96,6 +96,19 @@ import { PageNumberPagination, type PageNumberPaginationParams, PagePromise } fr import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +/** + * Manage tenants (your customers). + * + * Create a tenant for each of your customers to track their email sending separately. + * Store the tenant `id` in your database and use `metadata` for any custom data. + * + * **Quick Reference:** + * - `POST /tenants` - Create a new tenant + * - `GET /tenants` - List all tenants (paginated) + * - `GET /tenants/{id}` - Get tenant details + * - `PATCH /tenants/{id}` - Update tenant name, metadata, or status + * - `DELETE /tenants/{id}` - Delete a tenant + */ export class Tenants extends APIResource { credentials: CredentialsAPI.Credentials = new CredentialsAPI.Credentials(this._client); domains: DomainsAPI.Domains = new DomainsAPI.Domains(this._client); diff --git a/src/resources/tenants/tracking.ts b/src/resources/tenants/tracking.ts index 117b9d1..54e730f 100644 --- a/src/resources/tenants/tracking.ts +++ b/src/resources/tenants/tracking.ts @@ -7,6 +7,27 @@ import { APIPromise } from '../../core/api-promise'; import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +/** + * Manage track domains for open and click tracking. + * + * Track domains enable you to track when recipients: + * - Open your emails (tracking pixel) + * - Click links in your emails + * + * **Setup Process:** + * 1. Create a track domain with `POST /tracking` + * 2. Add the CNAME record to your DNS + * 3. Verify DNS with `POST /tracking/{id}/verify` + * 4. Track domain is ready when `dnsOk` is true + * + * **Quick Reference:** + * - `POST /tracking` - Create a new track domain + * - `GET /tracking` - List all track domains + * - `GET /tracking/{id}` - Get track domain details + * - `POST /tracking/{id}/verify` - Verify DNS configuration + * - `PATCH /tracking/{id}` - Enable/disable tracking features + * - `DELETE /tracking/{id}` - Remove a track domain + */ export class Tracking extends APIResource { /** * Create a new track domain for open/click tracking for a tenant. diff --git a/src/resources/tenants/usage.ts b/src/resources/tenants/usage.ts index b830f42..b21770e 100644 --- a/src/resources/tenants/usage.ts +++ b/src/resources/tenants/usage.ts @@ -7,6 +7,24 @@ import { APIPromise } from '../../core/api-promise'; import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +/** + * Per-tenant usage analytics and bulk reporting. + * + * Track email sending statistics for each tenant to power billing, dashboards, and monitoring. + * + * **Single Tenant Usage:** + * - `GET /tenants/{id}/usage` - Get usage stats for a specific tenant + * - `GET /tenants/{id}/usage/timeseries` - Get time-bucketed data for charts + * + * **Bulk Usage:** + * - `GET /usage/tenants` - Get usage for all tenants (paginated, sortable) + * - `GET /usage/export` - Export usage data as CSV, JSONL, or JSON + * + * **Period Formats:** + * - Shortcuts: `today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days` + * - Month: `2024-01` + * - Date range: `2024-01-01..2024-01-15` + */ export class Usage extends APIResource { /** * Returns email sending statistics for a specific tenant over a time period. diff --git a/src/resources/tenants/webhooks.ts b/src/resources/tenants/webhooks.ts index 8f1bd44..b03541c 100644 --- a/src/resources/tenants/webhooks.ts +++ b/src/resources/tenants/webhooks.ts @@ -6,6 +6,60 @@ import { APIPromise } from '../../core/api-promise'; import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +/** + * Configure webhook endpoints for real-time notifications. + * + * Webhooks notify your application when email events occur: + * - Email delivered, bounced, or failed + * - Email opened or link clicked + * - Spam complaint received + * + * **Quick Reference:** + * - `POST /webhooks` - Create a webhook endpoint + * - `GET /webhooks` - List all webhooks + * - `POST /webhooks/{id}/test` - Test a webhook with sample data + * - `PATCH /webhooks/{id}` - Update webhook configuration + * - `DELETE /webhooks/{id}` - Remove a webhook + * - `GET /webhooks/{id}/deliveries` - List delivery attempts + * - `GET /webhooks/{id}/deliveries/{deliveryId}` - Get delivery details + * - `POST /webhooks/{id}/deliveries/{deliveryId}/replay` - Replay a delivery + * + * ## Webhook Signatures + * + * All webhooks are cryptographically signed using RSA-SHA256 for security. + * Each webhook request includes: + * + * | Header | Description | + * |--------|-------------| + * | `X-Ark-Signature` | Base64-encoded RSA-SHA256 signature of the request body | + * | `X-Ark-Signature-KID` | Key ID identifying which public key was used | + * + * Verify signatures by fetching the public key from: + * ``` + * GET https://mail.arkhq.io/.well-known/jwks.json + * ``` + * + * ```javascript + * const crypto = require('crypto'); + * + * async function verifyWebhook(payload, signatureBase64, publicKey) { + * const signature = Buffer.from(signatureBase64, 'base64'); + * const verifier = crypto.createVerify('RSA-SHA256'); + * verifier.update(payload); + * return verifier.verify(publicKey, signature); + * } + * + * // In your webhook handler: + * const isValid = await verifyWebhook( + * rawBody, + * req.headers['x-ark-signature'], + * cachedPublicKey + * ); + * ``` + * + * **Important:** Always verify signatures before processing webhook data. + * See the [Webhook Integration Guide](/guides/webhook-integration) for complete examples. + */ export class Webhooks extends APIResource { /** * Create a webhook endpoint to receive email event notifications for a tenant. diff --git a/src/resources/usage.ts b/src/resources/usage.ts index c939386..bfdca19 100644 --- a/src/resources/usage.ts +++ b/src/resources/usage.ts @@ -7,6 +7,24 @@ import { APIPromise } from '../core/api-promise'; import { PageNumberPagination, type PageNumberPaginationParams, PagePromise } from '../core/pagination'; import { RequestOptions } from '../internal/request-options'; +/** + * Per-tenant usage analytics and bulk reporting. + * + * Track email sending statistics for each tenant to power billing, dashboards, and monitoring. + * + * **Single Tenant Usage:** + * - `GET /tenants/{id}/usage` - Get usage stats for a specific tenant + * - `GET /tenants/{id}/usage/timeseries` - Get time-bucketed data for charts + * + * **Bulk Usage:** + * - `GET /usage/tenants` - Get usage for all tenants (paginated, sortable) + * - `GET /usage/export` - Export usage data as CSV, JSONL, or JSON + * + * **Period Formats:** + * - Shortcuts: `today`, `yesterday`, `this_month`, `last_month`, `last_7_days`, `last_30_days` + * - Month: `2024-01` + * - Date range: `2024-01-01..2024-01-15` + */ export class Usage extends APIResource { /** * Returns aggregated email sending statistics for your entire organization. For diff --git a/src/version.ts b/src/version.ts index f3ae0bb..b4e51da 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.19.1'; // x-release-please-version +export const VERSION = '0.20.0'; // x-release-please-version diff --git a/tests/api-resources/emails.test.ts b/tests/api-resources/emails.test.ts index 3e97e51..e91dc6d 100644 --- a/tests/api-resources/emails.test.ts +++ b/tests/api-resources/emails.test.ts @@ -112,6 +112,7 @@ describe('resource emails', () => { metadata: { user_id: 'usr_123', campaign: 'onboarding' }, replyTo: 'dev@stainless.com', tag: 'tag', + tenantId: 'cm6abc123def456', text: 'text', 'Idempotency-Key': 'user_123_order_456', }); @@ -163,6 +164,7 @@ describe('resource emails', () => { }, ], from: 'notifications@myapp.com', + tenantId: 'cm6abc123def456', 'Idempotency-Key': 'user_123_order_456', }); }); @@ -188,6 +190,7 @@ describe('resource emails', () => { rawMessage: 'x', to: ['user@example.com'], bounce: true, + tenantId: 'cm6abc123def456', }); }); }); diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index 4620727..b6bf0b6 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -1,8 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Ark } from 'ark-email'; - -const { stringifyQuery } = Ark.prototype as any; +import { stringifyQuery } from 'ark-email/internal/utils/query'; describe(stringifyQuery, () => { for (const [input, expected] of [ @@ -15,7 +13,7 @@ describe(stringifyQuery, () => { 'e=f', )}=${encodeURIComponent('g&h')}`, ], - ]) { + ] as const) { it(`${JSON.stringify(input)} -> ${expected}`, () => { expect(stringifyQuery(input)).toEqual(expected); });