{
  "openapi": "3.0.1",
  "info": {
    "title": "Ken Client API",
    "description": "Welcome to the **Ken Client API v2** — the public, stable surface for building integrations\nagainst your Ken workspace. It exposes a focused, read-mostly slice of the platform:\ncampaigns, leads (including CSV/ZIP export), campaign analytics, suppression-list\n(blocklist) management, and a feed of past webhook events. The broader internal control\nsurface is **not** part of this API.\n\nEvery endpoint in this document is automatically scoped to the API key's client workspace.\nYou never pass a `clientId` — it is derived from the authenticated key, and resources that\nbelong to another workspace are reported as `404` rather than `403`.\n\n## Base path\n\nEvery endpoint in this document is served under the `/v2` prefix (for example,\n`GET /v2/campaigns`). Per-operation paths shown below omit the host but always include the\n`/v2` segment; prefix them with your Ken API base URL (for example, `https://api.getken.ai`)\nto form the full request URL.\n\n## Authentication\n\nEvery request must carry a Ken-issued API key as a Bearer token:\n\n```http\nAuthorization: Bearer sk_live_<your_key>\n```\n\nA key resolves to exactly one client workspace. If a key is linked to **no** workspace, or\nto **more than one**, requests are rejected with `403` — the v2 API requires a\nsingle-workspace key.\n\n### Getting and rotating a key\n\nAPI keys are provisioned from the Ken dashboard under **Settings → API Keys**. Each key is\nscoped to a single client workspace and inherits that workspace's data-access role. The key\nsecret is shown **only once** at creation. Rotate by creating a new key and revoking the\nold one; a revoked key immediately returns `401`.\n\n### Roles\n\nRead endpoints require only a valid key. **Lead export** (`GET /v2/leads/export`)\nadditionally requires the key's workspace role to be **Member or higher**; a valid key\nwhose user is view-only is rejected with `403`.\n\n## Response envelope\n\nSuccessful responses use one of three envelopes, all with `success: true`.\n\n**Single item** (`GET /v2/campaigns/{id}`, `GET /v2/analytics`, `POST /v2/blocklist`):\n\n```json\n{ \"success\": true, \"data\": { /* item */ } }\n```\n\n`GET /v2/events/types` also uses the single-item envelope, but its `data` is a JSON array\nof event-type strings rather than an object:\n\n```json\n{ \"success\": true, \"data\": [\"lead_replied\", \"lead_interested\", \"lead_unsubscribed\"] }\n```\n\n**Paginated list** (`GET /v2/leads`, `GET /v2/blocklist`, `GET /v2/events`):\n\n```json\n{\n  \"success\": true,\n  \"data\": [ /* page of items */ ],\n  \"pageIndex\": 1,\n  \"pageSize\": 50,\n  \"totalCount\": 187,\n  \"totalPages\": 4\n}\n```\n\n**Full (unpaginated) list with count** (`GET /v2/campaigns` only):\n\n```json\n{ \"success\": true, \"data\": [ /* all items */ ], \"total\": 12 }\n```\n\n`204 No Content` is returned with no body by `DELETE /v2/blocklist/{id}`. The `GET\n/v2/leads/export` endpoint returns a binary file (`text/csv` or `application/zip`), not a\nJSON envelope.\n\n## Error responses\n\nErrors raised by the API itself — model validation, workspace scoping, rate limiting, and\nthe endpoint handlers — use a uniform shape, including model-validation failures such as a\nnon-numeric `page`:\n\n```json\n{\n  \"success\": false,\n  \"message\": \"Campaign not found.\"\n}\n```\n\n| Status | Meaning                                                              |\n|--------|----------------------------------------------------------------------|\n| 400    | Validation failed (bad paging, invalid filter, malformed body).      |\n| 401    | Missing, malformed, expired, or revoked API key.                     |\n| 403    | Key is valid but not bound to a single workspace, or lacks the role for the requested action (e.g. export). |\n| 404    | Resource not found, or not owned by the authenticated client.        |\n| 429    | Rate limit exceeded (see below).                                     |\n| 5xx    | Transient server error — retry with exponential backoff.             |\n\n**Two framework-issued cases return only a status code with an empty body (no JSON\nenvelope):**\n\n- A request whose `Authorization` header is missing, is not a `Bearer` token, or carries a\n  JWT instead of an API key returns `401` with a `WWW-Authenticate: Bearer` header and no\n  body.\n- An authenticated request whose workspace role is below the level the action requires — for\n  example a view-only key calling `GET /v2/leads/export`, which needs Member or above —\n  returns `403` with no body.\n\nFor these two cases, branch on the HTTP status code, not the response body. (A `403` for a\nkey that is not bound to a single workspace, by contrast, still carries the JSON envelope\nabove.)\n\n## Rate limits\n\nLimits are enforced per API key:\n\n- **60 requests per minute** (short-burst ceiling).\n- **1000 requests per day** (daily ceiling).\n- `GET /v2/leads/export` is additionally capped at **10 requests per minute** because each\n  call can materialize a large file.\n\nExceeding a limit returns `429 Too Many Requests` with a `Retry-After` header giving the\nnumber of seconds to wait. Detect throttling by the **status code and `Retry-After`\nheader** rather than the body: the application limiter returns the JSON envelope above,\nwhile a 429 raised at the API gateway may carry a short plain-text body instead.\n\n## Pagination\n\nMost list endpoints share the same paging contract:\n\n- `page` — 1-based page number. Defaults to `1`; must be `1..10000`.\n- `pageSize` — items per page. Defaults to `50`; must be `1..500`.\n\nOut-of-range `page`/`pageSize` values are rejected with `400` (they are **not** clamped). A\nvalid page that lies past the last page returns an **empty** `200` page (not `404`). The\nresponse echoes the paging metadata alongside the `data` array (see \"Paginated list\"\nabove), where `pageIndex` is the page that was served.\n\n### Exception\n\n`GET /v2/campaigns` is **not** paginated: it accepts no paging parameters and returns the\nfull set of campaigns in a `{ success, data, total }` envelope. Client workspaces hold a\nbounded number of campaigns, so the list is returned in one response.\n\n## Webhooks\n\nKen can notify your systems in real time as outreach events happen, or you can\npoll for the same events on demand.\n\n### Consuming events\n\n- **Push (recommended).** Ken delivers each event as an HTTPS `POST` to an\n  endpoint you register. Delivery is powered by Svix, with automatic retries and\n  signed payloads. Endpoint registration is currently handled by the Ken team -\n  contact your Ken representative with the HTTPS URL(s) you want events sent to.\n- **Pull.** `GET /v2/events` returns the same events, paginated and filterable by\n  `eventType`, `campaignId`, and date range. `GET /v2/events/types` lists the\n  event types accepted by the `eventType` filter. Use this if you prefer polling\n  or want to backfill.\n\n### Event catalog\n\n| Event               | Fires when                                          |\n|---------------------|-----------------------------------------------------|\n| `email_sent`        | An outreach email is sent to a lead.                |\n| `lead_replied`      | A lead replies to an outreach email.                |\n| `lead_interested`   | A reply is classified as interested / positive.     |\n| `lead_unsubscribed` | A lead unsubscribes or opts out.                    |\n| `lead_clicked`      | A lead clicks a tracked link in an outreach email.  |\n\n### Payload shape\n\nEvery webhook delivery is a JSON object with the event type, a UTC timestamp,\nand an event-specific `data` block:\n\n```json\n{\n  \"type\": \"lead_replied\",\n  \"timestamp\": \"2026-04-20T16:05:42Z\",\n  \"data\": {\n    \"campaignId\": 1234,\n    \"leadId\": 56789,\n    \"email\": \"jane@acme.com\",\n    \"company\": \"Acme Inc.\"\n  }\n}\n```\n\n### Verifying signatures\n\nEach delivery carries Svix signature headers so you can verify it came from Ken\nand was not tampered with:\n\n- `svix-id` - unique message id.\n- `svix-timestamp` - Unix timestamp of the send.\n- `svix-signature` - one or more space-separated HMAC-SHA256 signatures.\n\nThe signature is an `HMAC-SHA256` over `{svix-id}.{svix-timestamp}.{raw_body}`,\nkeyed by your endpoint's signing secret (a value beginning with `whsec_`,\nprovided by Ken when your endpoint is registered). Reject deliveries whose\n`svix-timestamp` is outside a few minutes of now to prevent replay. The Svix\nopen-source verification libraries (`svix` for Python, Node, Go, and others)\nimplement this check for you.\n\n### Delivery semantics\n\nDeliveries are retried with backoff on non-`2xx` responses. Respond `2xx`\nquickly and process asynchronously. Events may arrive out of order and, rarely,\nmore than once - dedupe on `svix-id`.\n\n## Date handling\n\nAll timestamps are **UTC ISO-8601** (for example, `2026-04-20T16:05:42Z`). Date-range query\nparameters (`startDate`, `endDate`) accept either a date (`2026-04-20`) or a full datetime\n(`2026-04-20T16:05:42Z`). When both bounds are supplied they must be ordered and span no\nmore than **366 days**.\n\n`GET /v2/analytics` defaults to the **last 30 days** when no range is given, and because it\nfills in the missing bound (`endDate` → now, `startDate` → 30 days before `endDate`) *before*\nvalidating, the ordering and 366-day cap apply even when you supply only one of\n`startDate`/`endDate` — for example, a `startDate` more than 366 days before now is rejected\nwith `400`. For `GET /v2/events`, the cap and ordering are enforced only when **both** bounds\nare present.\n\n## Endpoint groups\n\n- **Campaigns** — list the campaigns in your workspace and read a single campaign's public\n  status and contact counts. Internal fields are never exposed.\n- **Leads** — page through your leads (optionally filtered by `campaignId` and a free-text\n  `search`), where each row is a flat object of selected columns; or export a campaign's\n  leads as CSV (one file) or ZIP (one CSV per segment).\n- **Analytics** — campaign engagement totals (sent, delivered, opened, uniqueOpened,\n  replied, uniqueReplied, bounced, interested, unsubscribed, clicked, uniqueClicked) with\n  derived rates, optionally broken down per day.\n- **Blocklist** — list, add, and remove suppression entries (email or domain). Entries take\n  effect in Ken immediately and, when the workspace has an EmailBison integration, are\n  mirrored to its blacklist.\n- **Webhook Events** — page through past business events for your campaigns (replies,\n  interested, unsubscribes) and discover the supported event-type filter values. This is a\n  pull/history feed; outbound webhook delivery to your own URLs is not part of this API.",
    "version": "v2"
  },
  "paths": {
    "/v2/analytics": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Get campaign analytics totals (and optionally a daily breakdown).",
        "description": "When campaignId is omitted, totals are aggregated across every campaign\r\nthe authenticated client owns. Date filters default to the last 30 days, and the requested\r\nwindow may span at most 366 days.\r\n            \r\nRates are computed by the v2 analytics aggregator: openRate = uniqueOpens / delivered,\r\nclickRate = uniqueClicks / delivered, replyRate = uniqueRepliers / delivered,\r\nbounceRate = bounced / sent, deliveryRate = delivered / sent. When unique counts are\r\nabsent (e.g. campaign-level rows produced by the EmailBison chart-polling fallback), the\r\nrate numerator falls back to the total event count so engagement is still reflected. Every\r\nrate is <b>0 when its denominator is 0</b>. This matches the polling document source, and\r\nis close to — but not identical with — `DailyCampaignStatsAggregationJob`, which for a\r\nfew rates falls back from a zero `delivered` denominator to `sent`; v2 does not.\r\nRates are returned as fractions in [0, 1].",
        "parameters": [
          {
            "name": "campaignId",
            "in": "query",
            "description": "Optional campaign id. Omit for client-wide totals.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "startDate",
            "in": "query",
            "description": "Optional UTC start date. Defaults to endDate minus 30 days.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "endDate",
            "in": "query",
            "description": "Optional UTC end date. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "granularity",
            "in": "query",
            "description": "`total` (default) or `daily`.",
            "schema": {
              "type": "string",
              "default": "total"
            }
          },
          {
            "name": "includeDaily",
            "in": "query",
            "description": "Alias for `granularity=daily`; when `true` the daily breakdown is included.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics payload for the requested window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiResponseOfClientAnalyticsDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid date range or granularity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "404": {
            "description": "Campaign does not belong to the authenticated client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/blocklist": {
      "get": {
        "tags": [
          "Blocklist"
        ],
        "summary": "List blocklist entries for the authenticated client.",
        "parameters": [
          {
            "name": "Page",
            "in": "query",
            "description": "1-based page number (1..10000). Defaults to 1.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "PageSize",
            "in": "query",
            "description": "Page size (1..500). Defaults to 50.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "Type",
            "in": "query",
            "description": "Optional filter: `email` or `domain`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Value",
            "in": "query",
            "description": "Optional exact value to match (email address or domain).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of blocklist entries (empty if the page is past the end).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiPagedResponseOfClientBlocklistDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid paging arguments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Blocklist"
        ],
        "summary": "Add a blocklist entry (email or domain).",
        "description": "Writes the email or domain to Ken's `do_not_contact_list` and, when the client has an\r\nEmailBison workspace configured, also syncs it to that workspace's blacklist so in-flight\r\nEmailBison campaigns honor the entry immediately. Clients without an EmailBison workspace\r\nstill receive a 200 with the entry persisted in Ken only.",
        "requestBody": {
          "description": "Entry payload.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientBlocklistCreateRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientBlocklistCreateRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ClientBlocklistCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Entry created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiResponseOfClientBlocklistDto"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (invalid type or value).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/blocklist/{id}": {
      "delete": {
        "tags": [
          "Blocklist"
        ],
        "summary": "Delete a blocklist entry by id.",
        "description": "When the client has an EmailBison workspace configured, also removes the corresponding\r\nentry from that workspace's blacklist.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Blocklist entry id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Entry removed."
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "404": {
            "description": "Entry not found or not owned by the authenticated client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/campaigns": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "List campaigns owned by the authenticated client.",
        "description": "Returns every campaign the scoped client owns, including drafts. The response is not\r\npaginated; client workspaces typically hold a bounded number of campaigns.",
        "responses": {
          "200": {
            "description": "Campaigns returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiListResponseOfClientCampaignDto"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/campaigns/{id}": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Get a single campaign by id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Ken campaign id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign returned successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiResponseOfClientCampaignDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid campaign id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "404": {
            "description": "Campaign not found or not owned by the authenticated client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/leads": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "List leads for the authenticated client.",
        "parameters": [
          {
            "name": "campaignId",
            "in": "query",
            "description": "Optional campaign filter. When omitted, returns leads across every campaign the client owns.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (1..10000). Defaults to 1.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Page size (1..500). Defaults to 50.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Optional free-text search over common lead fields (name, email, company).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of leads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiPagedResponseOfDictionaryOfStringAndObject"
                }
              }
            }
          },
          "400": {
            "description": "Invalid paging arguments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "404": {
            "description": "Campaign does not belong to the authenticated client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/leads/export": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "Export a campaign's leads.",
        "description": "Returns a CSV file named `leads_{campaignId}_{yyyyMMdd_HHmmss}.csv` for single-file\r\nexports. Segmented exports return a ZIP archive containing one CSV per segment. The\r\n`fields` parameter accepts column names from the internal export schema; when omitted,\r\nthe default field set is used.\r\n            \r\nUnlike the read-only v2 endpoints, export additionally requires the API key's workspace\r\nrole to be <b>Member or higher</b> (the `ExportAccess` policy). A valid key whose\r\nuser resolves to a view-only role is rejected with `403`.",
        "parameters": [
          {
            "name": "campaignId",
            "in": "query",
            "description": "Campaign id to export. Required.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Optional list of columns to include. Repeatable query parameter.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV or ZIP file download.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Missing campaignId or invalid export options.",
            "content": {
              "text/csv": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/zip": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "text/csv": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/zip": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks access to this client workspace, or its role is below Member (export requires Member+).",
            "content": {
              "text/csv": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/zip": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "404": {
            "description": "Campaign does not belong to the authenticated client.",
            "content": {
              "text/csv": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/zip": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/events": {
      "get": {
        "tags": [
          "Webhook Events"
        ],
        "summary": "List webhook events for the authenticated client's campaigns.",
        "parameters": [
          {
            "name": "eventType",
            "in": "query",
            "description": "Optional event type filter (see `GET /v2/events/types`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaignId",
            "in": "query",
            "description": "Optional campaign filter.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "startDate",
            "in": "query",
            "description": "Optional UTC start of the event window.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "endDate",
            "in": "query",
            "description": "Optional UTC end of the event window.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (1..10000). Defaults to 1.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Page size (1..500). Defaults to 50.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiPagedResponseOfClientEventDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid paging or date range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "404": {
            "description": "Campaign not found or not owned by the authenticated client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/events/types": {
      "get": {
        "tags": [
          "Webhook Events"
        ],
        "summary": "List every public event type accepted by the `eventType` filter.",
        "responses": {
          "200": {
            "description": "List of event type strings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiResponseOfString[]"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          },
          "403": {
            "description": "API key is valid but lacks access to this client workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ClientAnalyticsDailyDto": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "sent": {
            "type": "integer",
            "format": "int32"
          },
          "delivered": {
            "type": "integer",
            "format": "int32"
          },
          "opened": {
            "type": "integer",
            "format": "int32"
          },
          "uniqueOpened": {
            "type": "integer",
            "format": "int32"
          },
          "replied": {
            "type": "integer",
            "format": "int32"
          },
          "uniqueReplied": {
            "type": "integer",
            "format": "int32"
          },
          "bounced": {
            "type": "integer",
            "format": "int32"
          },
          "interested": {
            "type": "integer",
            "format": "int32"
          },
          "unsubscribed": {
            "type": "integer",
            "format": "int32"
          },
          "clicked": {
            "type": "integer",
            "format": "int32"
          },
          "uniqueClicked": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "example": {
          "date": "2026-04-20",
          "sent": 142,
          "delivered": 139,
          "opened": 91,
          "uniqueOpened": 78,
          "replied": 6,
          "uniqueReplied": 5,
          "bounced": 2,
          "interested": 3,
          "unsubscribed": 1,
          "clicked": 11,
          "uniqueClicked": 9
        }
      },
      "ClientAnalyticsDto": {
        "type": "object",
        "properties": {
          "granularity": {
            "type": "string",
            "nullable": true
          },
          "startDate": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endDate": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "campaignId": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "totals": {
            "$ref": "#/components/schemas/ClientAnalyticsTotalsDto"
          },
          "daily": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientAnalyticsDailyDto"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "example": {
          "granularity": "total",
          "startDate": "2026-03-21T00:00:00.0000000+00:00",
          "endDate": "2026-04-20T00:00:00.0000000+00:00",
          "campaignId": 482913,
          "totals": {
            "sent": 2143,
            "delivered": 2101,
            "opened": 1284,
            "uniqueOpened": 1097,
            "replied": 83,
            "uniqueReplied": 76,
            "bounced": 42,
            "interested": 29,
            "unsubscribed": 11,
            "clicked": 157,
            "uniqueClicked": 141,
            "deliveryRate": 0.9804,
            "openRate": 0.5221,
            "replyRate": 0.0362,
            "bounceRate": 0.0196,
            "clickRate": 0.0671
          },
          "daily": [
            {
              "date": "2026-04-20",
              "sent": 142,
              "delivered": 139,
              "opened": 91,
              "uniqueOpened": 78,
              "replied": 6,
              "uniqueReplied": 5,
              "bounced": 2,
              "interested": 3,
              "unsubscribed": 1,
              "clicked": 11,
              "uniqueClicked": 9
            }
          ]
        }
      },
      "ClientAnalyticsTotalsDto": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "integer",
            "format": "int32"
          },
          "delivered": {
            "type": "integer",
            "format": "int32"
          },
          "opened": {
            "type": "integer",
            "format": "int32"
          },
          "uniqueOpened": {
            "type": "integer",
            "format": "int32"
          },
          "replied": {
            "type": "integer",
            "format": "int32"
          },
          "uniqueReplied": {
            "type": "integer",
            "format": "int32"
          },
          "bounced": {
            "type": "integer",
            "format": "int32"
          },
          "interested": {
            "type": "integer",
            "format": "int32"
          },
          "unsubscribed": {
            "type": "integer",
            "format": "int32"
          },
          "clicked": {
            "type": "integer",
            "format": "int32"
          },
          "uniqueClicked": {
            "type": "integer",
            "format": "int32"
          },
          "deliveryRate": {
            "type": "number",
            "format": "double"
          },
          "openRate": {
            "type": "number",
            "format": "double"
          },
          "replyRate": {
            "type": "number",
            "format": "double"
          },
          "bounceRate": {
            "type": "number",
            "format": "double"
          },
          "clickRate": {
            "type": "number",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "example": {
          "sent": 2143,
          "delivered": 2101,
          "opened": 1284,
          "uniqueOpened": 1097,
          "replied": 83,
          "uniqueReplied": 76,
          "bounced": 42,
          "interested": 29,
          "unsubscribed": 11,
          "clicked": 157,
          "uniqueClicked": 141,
          "deliveryRate": 0.9804,
          "openRate": 0.5221,
          "replyRate": 0.0362,
          "bounceRate": 0.0196,
          "clickRate": 0.0671
        }
      },
      "ClientApiError": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "example": {
          "success": false,
          "message": "Campaign not found."
        }
      },
      "ClientApiListResponseOfClientCampaignDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientCampaignDto"
            },
            "nullable": true
          },
          "total": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": [
            {
              "id": 482913,
              "name": "Q2 outbound — Series B SaaS founders",
              "status": "Sending",
              "contactLimit": 2500,
              "validContactsFound": 2143,
              "createdDatetime": "2026-04-03T14:22:11.0000000+00:00"
            }
          ],
          "total": 1
        }
      },
      "ClientApiPagedResponseOfClientBlocklistDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientBlocklistDto"
            },
            "nullable": true
          },
          "pageIndex": {
            "type": "integer",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "format": "int32"
          },
          "totalCount": {
            "type": "integer",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": [
            {
              "id": 7712,
              "type": "domain",
              "value": "competitor-corp.com"
            }
          ],
          "pageIndex": 1,
          "pageSize": 50,
          "totalCount": 1,
          "totalPages": 1
        }
      },
      "ClientApiPagedResponseOfClientEventDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientEventDto"
            },
            "nullable": true
          },
          "pageIndex": {
            "type": "integer",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "format": "int32"
          },
          "totalCount": {
            "type": "integer",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": [
            {
              "eventType": "lead_replied",
              "timestamp": "2026-04-20T16:05:42.0000000+00:00",
              "campaignId": 482913,
              "campaignName": "Q2 outbound — Series B SaaS founders",
              "leadId": 90125,
              "leadEmail": "amanda.chen@northwind-labs.com",
              "leadFullName": null,
              "leadCompany": null
            }
          ],
          "pageIndex": 1,
          "pageSize": 50,
          "totalCount": 1,
          "totalPages": 1
        }
      },
      "ClientApiPagedResponseOfDictionaryOfStringAndObject": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": { },
              "example": {
                "id": 90125,
                "first_name": "Amanda",
                "last_name": "Chen",
                "full_name": "Amanda Chen",
                "email": "amanda.chen@northwind-labs.com",
                "email_validity": "Verified",
                "title": "VP of Engineering",
                "profile_url": "https://www.linkedin.com/in/amanda-chen-northwind",
                "company_name": "Northwind Labs",
                "company_domain": "northwind-labs.com",
                "campaign_name": "Q2 outbound — Series B SaaS founders"
              }
            },
            "nullable": true
          },
          "pageIndex": {
            "type": "integer",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "format": "int32"
          },
          "totalCount": {
            "type": "integer",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": [
            {
              "id": 90125,
              "first_name": "Amanda",
              "last_name": "Chen",
              "full_name": "Amanda Chen",
              "email": "amanda.chen@northwind-labs.com",
              "email_validity": "Verified",
              "title": "VP of Engineering",
              "profile_url": "https://www.linkedin.com/in/amanda-chen-northwind",
              "company_name": "Northwind Labs",
              "company_domain": "northwind-labs.com",
              "campaign_name": "Q2 outbound — Series B SaaS founders"
            }
          ],
          "pageIndex": 1,
          "pageSize": 50,
          "totalCount": 1,
          "totalPages": 1
        }
      },
      "ClientApiResponseOfClientAnalyticsDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "$ref": "#/components/schemas/ClientAnalyticsDto"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": {
            "granularity": "total",
            "startDate": "2026-03-21T00:00:00.0000000+00:00",
            "endDate": "2026-04-20T00:00:00.0000000+00:00",
            "campaignId": 482913,
            "totals": {
              "sent": 2143,
              "delivered": 2101,
              "opened": 1284,
              "uniqueOpened": 1097,
              "replied": 83,
              "uniqueReplied": 76,
              "bounced": 42,
              "interested": 29,
              "unsubscribed": 11,
              "clicked": 157,
              "uniqueClicked": 141,
              "deliveryRate": 0.9804,
              "openRate": 0.5221,
              "replyRate": 0.0362,
              "bounceRate": 0.0196,
              "clickRate": 0.0671
            },
            "daily": [
              {
                "date": "2026-04-20",
                "sent": 142,
                "delivered": 139,
                "opened": 91,
                "uniqueOpened": 78,
                "replied": 6,
                "uniqueReplied": 5,
                "bounced": 2,
                "interested": 3,
                "unsubscribed": 1,
                "clicked": 11,
                "uniqueClicked": 9
              }
            ]
          }
        }
      },
      "ClientApiResponseOfClientBlocklistDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "$ref": "#/components/schemas/ClientBlocklistDto"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": {
            "id": 7712,
            "type": "domain",
            "value": "competitor-corp.com"
          }
        }
      },
      "ClientApiResponseOfClientCampaignDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "$ref": "#/components/schemas/ClientCampaignDto"
          }
        },
        "additionalProperties": false,
        "example": {
          "success": true,
          "data": {
            "id": 482913,
            "name": "Q2 outbound — Series B SaaS founders",
            "status": "Sending",
            "contactLimit": 2500,
            "validContactsFound": 2143,
            "createdDatetime": "2026-04-03T14:22:11.0000000+00:00"
          }
        }
      },
      "ClientApiResponseOfString[]": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ClientBlocklistCreateRequest": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "value": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "example": {
          "type": "email",
          "value": "unsubscribed@example.com"
        }
      },
      "ClientBlocklistDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "type": {
            "type": "string",
            "nullable": true
          },
          "value": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "example": {
          "id": 7712,
          "type": "domain",
          "value": "competitor-corp.com"
        }
      },
      "ClientCampaignDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "contactLimit": {
            "type": "integer",
            "format": "int32"
          },
          "validContactsFound": {
            "type": "integer",
            "format": "int32"
          },
          "createdDatetime": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "example": {
          "id": 482913,
          "name": "Q2 outbound — Series B SaaS founders",
          "status": "Sending",
          "contactLimit": 2500,
          "validContactsFound": 2143,
          "createdDatetime": "2026-04-03T14:22:11.0000000+00:00"
        }
      },
      "ClientEventDto": {
        "type": "object",
        "properties": {
          "eventType": {
            "type": "string",
            "nullable": true
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "campaignId": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "campaignName": {
            "type": "string",
            "nullable": true
          },
          "leadId": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "leadEmail": {
            "type": "string",
            "nullable": true
          },
          "leadFullName": {
            "type": "string",
            "nullable": true
          },
          "leadCompany": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "example": {
          "eventType": "lead_replied",
          "timestamp": "2026-04-20T16:05:42.0000000+00:00",
          "campaignId": 482913,
          "campaignName": "Q2 outbound — Series B SaaS founders",
          "leadId": 90125,
          "leadEmail": "amanda.chen@northwind-labs.com",
          "leadFullName": null,
          "leadCompany": null
        }
      }
    },
    "securitySchemes": {
      "Bearer": {
        "type": "http",
        "description": "Enter: Bearer {your Clerk API key for Client API v2, or staff JWT for internal v1 endpoints}",
        "scheme": "bearer",
        "bearerFormat": "Opaque API key or JWT"
      }
    }
  },
  "security": [
    {
      "Bearer": [ ]
    }
  ],
  "tags": [
    {
      "name": "Analytics",
      "description": "Client-facing campaign analytics. Aggregates MongoDB daily stats for the scoped\r\nclient across one or all of their campaigns."
    },
    {
      "name": "Blocklist",
      "description": "Client-facing blocklist endpoints. Entries are written to the Ken `do_not_contact_list`\r\ntable and, when the client has an EmailBison workspace configured, are also synced to that\r\nworkspace's blacklist so in-flight EmailBison campaigns respect the update immediately.\r\nClients without an EmailBison workspace still get a successful response with the entry\r\npersisted in Ken only."
    },
    {
      "name": "Campaigns",
      "description": "Client-facing campaigns endpoints. Scoped to the authenticated API key's client\r\nworkspace via Ken.Scraper.API.Filters.ClientScopeActionFilter; callers never pass client_id."
    },
    {
      "name": "Webhook Events",
      "description": "Client-facing webhook events feed. Reads the configured business webhook-events\r\ncollection (resolved from `MongoDbConfiguration`) scoped to the authenticated\r\nclient's campaigns."
    },
    {
      "name": "Leads",
      "description": "Client-facing leads endpoints. ClientId comes from Ken.Scraper.API.Filters.ClientScopeActionFilter;\r\ncampaign-scoped requests validate that the campaign belongs to the scoped client before\r\ndispatching."
    }
  ]
}