# Co-Signal API: the consolidated Phase 26 checkpoint 1 contract (ECO-01, D-36).
#
# This is the ONE published description of the Co-Signal REST API. It
# describes exactly the routes that exist under src/app/api/v1 and nothing
# more. tests/design/openapi-parity.test.ts enforces that in both directions
# (a shipped route that is not documented and a documented path that has no
# route are both failing tests) and binds every scope, principal restriction,
# error code, limitation string and response property here to the module that
# enforces it (src/lib/ecosystem/operation-policy.ts and
# src/lib/ecosystem/contracts.ts).
#
# Release state: ECOSYSTEM_API_ENABLED is on in production since
# rollout-2026-09-24-003 (D-63), and the API answers only the workspaces on
# the rollout allowlist (D-37); a valid key of any other workspace gets
# 503 feature_disabled. Block 2 changes (September 2026) are listed in
# docs/api/CHANGELOG.md.
#
# Route names are Co-Signal design choices. Endpoint inventory and semantic
# coverage are authoritative; nothing here promises drop in wire compatibility
# with any other vendor's API (26-SPEC.md).
openapi: 3.0.3
info:
  title: Co-Signal API
  version: "2026-09-20"
  description: |-
    Read access to your own workspace's accounts, leads, contacts,
    opportunities, partners, populations and account overlaps, for scripts
    and automation tools. The public getting started guide is
    https://co-signal.com/developers.

    **Availability.** The API is released per workspace on top of a global
    switch (D-37). For a valid bearer, a workspace outside the rollout
    receives the byte identical `503 feature_disabled` that a globally
    disabled API returns, so that answer does not say which of the two
    applies. Whether the global switch is on is public, though: while it is
    off every request gets `503 feature_disabled` before any token is read,
    and while it is on a missing or invalid token gets `401 invalid_token`,
    the token endpoint accepts the API as a resource and the API's protected
    resource metadata is served. The global switch is on (B-06).

    **Authentication.** An admin creates an integration key at
    Settings > API and copies its `csk_` secret once. The key is exchanged for
    a 10 minute access token at the token endpoint, sending its ID and secret
    either as HTTP Basic authentication or as form fields, never both (see
    the `ecosystemClientCredentials` security scheme), and every request
    below presents that token as `Authorization: Bearer <token>`. A signed
    in member can instead consent in Co-Signal to a token of their own, with
    the limits the same security scheme describes.

    **Identifiers.** Every id on the wire is a stable, opaque, tenant bound
    public id with a resource prefix (`acct_`, `lead_`, `cont_`, `opp_`,
    `ptsh_`, `pop_`, `ws_`). No Firestore document id and no CRM record id is
    ever returned. The ids are the join key inside your own id space; to go
    from a CRM record to a Co-Signal id, use `GET /leads/resolve`, which takes
    the CRM reference as INPUT. An id issued to another workspace answers the
    same `404 not_found` as an id that was never issued.

    **Freshness (D-12).** Every successful read response carries three separate times in
    `meta`: `sourceUpdatedAt` (when the CRM last changed the data, or null
    when no source timestamp exists), `observedAt` (when Co-Signal last
    observed it) and `computedAt` (when this answer was calculated). A fast
    response does not mean fresh data. No route fills one of these from
    another. On the reads of your own accounts, contacts, opportunities and
    leads, `meta.sourceUpdatedAt` is the latest CRM edit time among the rows
    returned and each row's provenance carries its record's own edit time,
    null for a record that has none; `no_crm_source_timestamp` is declared
    exactly when a returned row has none. Those reads also carry
    `meta.sourceFreshness`: for each connected CRM, whether its data is
    `fresh`, `stale` or `degraded`, and a degraded connection is never shown
    as fresh (the two batch reads, `POST /accounts/batch` and
    `POST /leads/batch`, do not carry it). Every other feed keeps
    `meta.sourceUpdatedAt` null: the overlaps, partners, identity and account
    context reads declare `no_crm_source_timestamp` with it, and the change
    feed declares none, because its rows carry their own provenance time. A
    row's provenance `observedAt` is when Co-Signal last synced that record. Until
    September 2026 the accounts list, the overlaps feed, the account context
    and the three lead routes carried that sync time in `sourceUpdatedAt`
    (B-02).

    **Absent, null and withheld (D-07).** A key left out of `required` below
    may be OMITTED from a response; a key documented as `nullable` is present
    with null when the source asserted nothing. The two never share a
    meaning.

    **Size guards.** Every `maxLength` in this document is a response SIZE
    GUARD that keeps one oversized row from failing a page. It is not a
    validation rule a client may rely on, and a later release may raise any
    of them. The values are 500 for display names and titles, 320 for an
    email, 253 for a domain, 255 for a lead `status` and an opportunity
    `stage`, 200 for a field key, 4000 for a lead field string value, and no
    bound at all on a stored population filter value.

    **Pagination.** List feeds return `{ data, page, meta }`. `limit` defaults
    to 25 and its maximum is 200; a `limit` outside 1 to 200 is
    `400 invalid_request`, never a silent clamp. The SPEC's larger page cap of
    1000 is a LATER RELEASE target, not a current capability, because no
    cursor stable snapshot exists yet. See `ListCursor` for the cursor
    contract. No response carries an exact total.

    **Rate limits.** 120 requests a minute for one key and 600 a minute for
    one workspace, counted in fixed 60 second windows. Over either limit the
    answer is `429 rate_limited` with `Retry-After` in seconds until the
    window ends. The limit is keyed on who is calling, never on what was
    asked for. The token endpoint has its own budget of 60 client
    credentials exchanges a minute per verified key and a separate
    per-instance credential check ceiling (see the security scheme).

    **Webhooks.** An admin created key can register an HTTPS endpoint that
    Co-Signal sends signed, thin events to (`/webhook-subscriptions`), after a
    challenge handshake that proves the endpoint is yours. A delivery carries
    the event type, its time and public ids only (the one exception is
    `attribution.updated`, which also carries the names of the partners credited
    on a deal and the deal's value, see `AttributionEventData`), signed with
    HMAC-SHA256 over
    the timestamp, a dot and the raw body; the `WebhookEventEnvelope` component
    and the guide at https://co-signal.com/developers#webhooks describe the
    format and the verification code. The delivery log of a subscription
    (`/webhook-subscriptions/{subscriptionId}/deliveries`) lists every
    delivery, and a delivery that ended can be replayed or discarded
    (`/deliveries/{deliveryId}/replay` and `/deliveries/{deliveryId}/discard`).
    Webhooks are not yet enabled for every
    workspace, and until yours is released their routes answer `503
    feature_disabled`.

    **Signals.** The signal feeds (`/signals/accounts`, `/signals/leads` and
    `/signals/own-deals`) are the pull side of webhooks: the same signals,
    read with a snapshot and then a bookmark, so a system that cannot receive
    a webhook, or that missed some, can catch up. A feed follows the change
    feed's cursor rules and `410` answers, and the guide at
    https://co-signal.com/developers#signal-feeds describes the rules. Signal
    feeds are not yet enabled for every workspace, and until yours is
    released their routes answer `503 feature_disabled`.

    **What this API does not do.** An integration key never writes to your
    CRM. The only CRM writes this API can start are the two CRM commands,
    `POST /crm/accounts/{accountId}/push-fields` and
    `POST /crm/overlaps/{overlapId}/sync`: Co-Signal extensions that are off
    by default, are released per workspace, can be run only by a workspace
    admin's approved app that an admin switched the command on for, and write
    only a previewed payload whose hash you send back. A partner's data flows
    through this API only once that partner's owner has switched the
    automation channel on for the partnership, and that channel is OFF by
    default for every partnership (D-30).
servers:
  - url: https://app.co-signal.com/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local development
tags:
  - name: Accounts
  - name: Leads
  - name: Contacts
  - name: Opportunities
  - name: Partners
  - name: Populations
  - name: Overlaps
  - name: Knowledge
  - name: Identity
  - name: Changes
  - name: Reports
  - name: Lists
  - name: Greenfield
  - name: Recommendations
  - name: Attribution
  - name: Activity
  - name: Suggestions
  - name: Actions
  - name: Exports
  - name: Imports
  - name: Webhooks
  - name: Signals
paths:
  /accounts:
    get:
      operationId: accounts_list
      x-cosignal-operation: accounts_list
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Accounts]
      summary: List your own accounts.
      description: >-
        Returns your own workspace's accounts, one `AccountSummary` per
        account, in a stable internal order that is not a name sort. It does
        not return partner accounts, contacts or opportunities (use
        `/contacts`, `/opportunities` and `/overlaps/accounts`), and it
        accepts no filter or sort. Only `limit` and `cursor` are accepted: an
        unknown or repeated query parameter is `400 invalid_request`. Every
        page states `order_is_stable_not_alphabetical`, and
        `no_crm_source_timestamp` when a returned account has no CRM edit
        time. `meta.sourceUpdatedAt` is the latest CRM edit time among the
        accounts returned, each provenance `sourceUpdatedAt` is that account's
        own (null for the open opportunity figures, which Co-Signal
        computes), and each provenance `observedAt` is when Co-Signal last
        synced that account. `meta.sourceFreshness` states, for each
        connected CRM, whether its data is fresh, stale or degraded. A stored
        account whose display name is blank is
        served as `Unnamed company`, with that name's provenance `sharing`
        of `absent`.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your own accounts.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /accounts/resolve:
    get:
      operationId: accounts_resolve
      x-cosignal-operation: accounts_resolve
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Accounts]
      summary: Find your own account from company evidence.
      description: >-
        Takes company evidence as INPUT and returns the matching accounts of
        your own workspace in the list envelope, with `page.hasMore` false and
        `page.nextCursor` null: a resolve is a lookup with no page to continue
        from, so it accepts no `limit` and never mints a cursor.

        Exactly ONE selector per request:

        - the pair `source` plus `sourceRecordId` (either half alone names
          nothing), which returns every account in your workspace carrying
          that CRM record; a Salesforce id is matched in its
          15 or 18 character form,
        - a company `domain`, which returns the account whose identity is that
          domain in your workspace, or
        - a company `name`, which returns the accounts whose stored name
          matches within the same leading characters (see the `name`
          parameter for exactly what that means).

        A `domain` must be company evidence. A personal mail domain, such as
        `gmail.com`, is refused with `400 invalid_request`, because a free mail
        domain identifies a person and never a company, and so is a string that
        does not resolve to a registrable domain.

        Resolution returns every match as a candidate rather than choosing one,
        so this route never guesses between accounts, and an ambiguous name
        answers with up to ten rows carrying
        `results_capped_by_request_budget` in `meta.limitations` with `hasMore`
        still false.

        A selector that names something in another workspace, and a selector
        that names nothing at all, answer with the same empty page: the
        response never tells you which of the two you are in. A token held by
        a workspace member, rather than an admin or an integration key,
        resolves only the accounts that member owns.

        Only the parameters documented here are accepted: an unknown or
        repeated query parameter, including `limit` and `cursor`, is
        `400 invalid_request`. `meta.sourceUpdatedAt` is the latest CRM
        edit time among the accounts matched (null with
        `no_crm_source_timestamp` when a matched account has none),
        `meta.sourceFreshness` states each connected CRM's freshness, and each
        row's provenance `observedAt` is when Co-Signal last synced that
        account.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read]
      parameters:
        - name: source
          in: query
          required: false
          description: The CRM the reference comes from. Must be sent together with `sourceRecordId`.
          schema:
            type: string
            enum: [salesforce, hubspot]
        - name: sourceRecordId
          in: query
          required: false
          description: The account record id in that CRM. Must be sent together with `source`. Every account carrying it is returned, so a record that was re-ingested under a new domain returns both.
          schema:
            type: string
            minLength: 1
            maxLength: 256
        - name: domain
          in: query
          required: false
          description: A company domain. Never sent together with any other selector. A personal mail domain is refused.
          schema:
            type: string
            minLength: 1
            maxLength: 253
        - name: name
          in: query
          required: false
          description: >-
            A company name. Never sent together with any other selector.
            Matching is case, punctuation and legal suffix tolerant within
            the same leading characters only: the comparison is on the
            name's normalized form, which is lower cased, has periods and
            commas removed and drops its legal suffix words, so a stored
            `Acme, Inc.`, a stored `ACME Inc` and a stored lower case
            `acme` all answer a query of `acme`, and a name carrying a
            capital after the first letter of a word (a stored `GenePoint`)
            answers a query of `GenePoint`, `genepoint` or `GENEPOINT`
            alike. A query of `acm` finds names beginning `Acme` as
            candidates. There is no substring matching and no similarity
            ranking, so a reworded name is not matched. A name that matches
            several of your own accounts returns every match, and only the
            first ten.
          schema:
            type: string
            minLength: 1
            maxLength: 500
      responses:
        "200":
          description: Zero or more of your own accounts, never a guess.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /accounts/batch:
    post:
      operationId: accounts_batch
      x-cosignal-operation: accounts_batch
      x-cosignal-principal-restriction: none
      x-cosignal-availability: released_per_workspace
      tags: [Accounts]
      summary: Resolve up to 100 account selectors in one call.
      description: >-
        Takes up to 100 selectors in a JSON body and answers one item per
        selector, in the order you sent them. Each selector is the same object
        `GET /accounts/resolve` takes as its query: a company `domain`, the
        pair `source` plus `sourceRecordId`, or a company `name`, exactly one
        per selector. An item's `candidates` and `limitations` are exactly the
        `data` and `meta.limitations` that `GET /accounts/resolve` returns for
        that selector alone, for the same caller, so a batch never reveals
        more than the same selectors resolved one at a time: the same domain
        rules, the same ten candidate cap, the same narrowing to a member's own
        accounts, and the same empty answer for an account that exists only in
        another workspace as for one that exists nowhere.

        An item's `status` is `matched` (one candidate), `ambiguous` (two or
        more, never a guess), `none` (no candidate) or `invalid`. A selector the
        single resolve would refuse with `400 invalid_request` (a personal mail
        domain, two selectors in one object, half a CRM reference, an unknown
        key) is an `invalid` item with `error` `invalid_request`, no candidates
        and no limitations, and the other selectors are still resolved. A body
        that holds no selector, more than 100, a key other than `selectors`,
        or anything that is not JSON is `400 invalid_request` before any
        selector is read.

        A batch is read only: it takes no `Idempotency-Key`, writes nothing of
        yours and counts once against the per minute rate limit. It consumes
        its selector count (invalid selectors included) from the workspace's
        `batch_selectors_per_day` budget of 50,000 selectors per UTC day, shared
        with `POST /leads/batch`, before any selector is resolved. A batch that
        would pass the budget is refused whole with `429 budget_exceeded`, the
        budget's numbers and `Retry-After` seconds until 00:00 UTC, and is
        never answered partially. Selectors are resolved with at most ten in
        flight.

        Batch resolution is not yet enabled for every workspace. Until your
        workspace is released a call answers `503 feature_disabled`, byte for
        byte the answer of a workspace outside the API rollout. Requires
        `ecosystem:accounts:read`.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchResolveRequest"
      responses:
        "200":
          description: One item per selector, in input order.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountBatchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/BatchLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /accounts/{accountId}:
    get:
      operationId: accounts_detail
      x-cosignal-operation: accounts_detail
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Accounts]
      summary: Read one of your own accounts.
      description: >-
        One account of your own workspace, by its `acct_` public id, in exactly
        the row shape `GET /api/v1/accounts` serves for the same account, with
        the same freshness block as every other detail response and no `page`:
        a detail names one record, so there is no cursor to continue from.

        `meta.limitations` is `["no_crm_source_timestamp"]` when the account
        has no CRM edit time, and empty when it has one. `sourceUpdatedAt` is
        that CRM edit time or null, `meta.sourceFreshness` states each
        connected CRM's freshness, `observedAt` is when Co-Signal last synced
        the account, and `computedAt` is when this answer was computed. The
        stable order limitation the accounts list carries is not repeated
        here, because a single record has no order to describe.

        A blank stored name is served as `Unnamed company`, with that name's
        provenance `sharing` of `absent`. That is deliberately different from
        `GET /api/v1/accounts/{accountId}/context`, which still answers
        `503 temporarily_unavailable` for such an account: this is the route
        to read your own account.

        No partner, contact, opportunity or custom field section appears
        here. This route reads no partner owned record, so its disclosure
        surface equals the accounts list's, and
        `GET /api/v1/accounts/{accountId}/context` stays the richer partner
        aware answer.

        `accountId` must be an `acct_` public id: anything else is
        `400 invalid_request` on shape alone, before any lookup. The route
        accepts no query parameter at all, so a `limit` or a `cursor` is
        `400 invalid_request` too. An id that was never issued, was issued to
        another workspace, names a deleted account, or names an account you
        may not see answers ONE `404 not_found`: an absent id and an
        unauthorized id are deliberately indistinguishable.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read]
      parameters:
        - $ref: "#/components/parameters/AccountId"
      responses:
        "200":
          description: The account, in the accounts list's row shape.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /accounts/{accountId}/context:
    get:
      operationId: accounts_context
      x-cosignal-operation: accounts_context
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Accounts]
      summary: Read one account with its authorized partner context.
      description: >-
        The richer per account read that predates the list feeds: your account
        plus every partner that currently contributes record level context
        for it, with that partner's contacts and opportunities only where
        your key also holds `ecosystem:contacts:read` or
        `ecosystem:opportunities:read` AND the partner's own sharing grant
        and automation channel allow it. A partner that shares counts only
        contributes nothing, and is indistinguishable from no match.
        `accountId` must be an `acct_` public id: anything else is
        `400 invalid_request` on shape alone, before any lookup. An id that
        was never issued, was issued to another workspace or names a missing
        account answers the same `404 not_found`. A member's own token reads
        only an account that member owns or claimed, the same view the app
        shows them, and any other account answers that same `404 not_found`;
        an admin created key or a signed in admin reads any account of the
        workspace. The bare
        `GET /accounts/{accountId}` serves the account itself in the accounts
        list's own row shape; this route stays the richer, partner aware
        answer, and the partners that overlap it are listed by
        `GET /accounts/{accountId}/overlapping-partners`.
        `meta.limitations` on this route is
        free text rather than the closed `ListLimitation` vocabulary, and its
        first member is always `no_crm_source_timestamp`: `meta.sourceUpdatedAt`
        and your own account's provenance `sourceUpdatedAt` are null, and
        that provenance's `observedAt` is when Co-Signal last synced the
        account. Known limit: a blank stored name on your own account still
        answers `503 temporarily_unavailable` on this route. In each partner
        entry, `id` is the partner's matching ACCOUNT as an `acct_` public id
        while `name` is the partner WORKSPACE's name, and `customFields`
        carries the account fields that partner chose, ticked on a sharing
        rule above counts and shares through Automation (D-60, D-28).
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read]
      parameters:
        - $ref: "#/components/parameters/AccountId"
      responses:
        "200":
          description: The authorized account brief.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountContextResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /accounts/{accountId}/overlapping-partners:
    get:
      operationId: overlapping_partners_read
      x-cosignal-operation: overlapping_partners_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Accounts]
      summary: List the partners that overlap one of your accounts.
      description: |-
        One row per PARTNERSHIP that currently contributes a qualifying
        overlap for the account in the path, the REST half of the
        `find_overlapping_partners` assistant tool. Requires both
        `ecosystem:accounts:read` and `ecosystem:partners:read`.

        Each row's `partner` is exactly the entry
        `GET /overlaps/accounts` serves for that account and partnership,
        so the two routes can never disclose differently for the same
        caller. `tags` are YOUR workspace's own private tags on that
        partnership (never the partner's), sorted by name. When a
        partnership has more than one matching partner account, the row
        carries the strongest match.

        **Partner scores are an admin view.** `score` is the partner's
        overall score from the stored score snapshot, present only for an
        admin created key or a signed in admin, and only when a score is
        stored for that partnership. A member's own token never receives
        `score` and is always ordered by name; every member response
        carries `partner_scores_admin_view_only` in `meta.limitations`,
        whether or not any partner has a score. Do not confuse it with
        `partner.score`, which is the match score of the overlap and is
        served to every caller.

        **Order.** `sort=score` (the default for an admin) orders by
        `score` descending with an absent score last, then the lower cased
        partner name, then the `ptsh_` id. `sort=name` orders by the lower
        cased partner name, then the `ptsh_` id. Two identical requests
        return the identical order and page boundaries. `partner` (one
        `ptsh_` id) and `partnerTag` (one of your own `ptag_` ids) narrow
        the rows; an id that does not resolve in your own audience (unknown,
        malformed or another workspace's) is `400 invalid_request`, all one
        identical answer.

        **Channel.** This route reads through the automation channel, which
        is OFF by default for every partnership (D-30). A partner that has not
        switched on Automation access, a partner that shares counts only, a
        partner with no sharing rule and a partnership that has ended are
        each simply absent from `data`, with no error, marker or count
        difference. An account with no overlapping partner answers `200`
        with an empty `data`.

        `accountId` must be an `acct_` public id: anything else is
        `400 invalid_request` on shape alone, before any lookup. An id that
        was never issued, was issued to another workspace or names a missing
        account answers the same `404 not_found`. A member's own token reads
        only an account that member owns or claimed, and any other account
        answers that same `404 not_found`. Overlaps are computed for this
        request (`overlap_recomputed_per_request`), so `computedAt` is the
        computation moment. Only `partner`, `partnerTag`, `sort`,
        `limit` and `cursor` are accepted.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/AccountId"
        - name: partner
          in: query
          required: false
          description: >-
            A `ptsh_` partnership id: only the row for that partnership.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partnerTag
          in: query
          required: false
          description: >-
            A `ptag_` id of one of your workspace's own partner tags: only
            rows for partnerships your workspace tagged with it. A deleted
            tag's id returns an empty page.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: sort
          in: query
          required: false
          description: >-
            `score` or `name`. Ordering by `score` is an admin view, and it is
            the default for an admin. A member's list is always ordered by
            name, whatever is sent, and `sort=score` from a member is not an
            error.
          schema:
            type: string
            enum: [score, name]
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the partners that overlap the account.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OverlappingPartnersResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /leads:
    get:
      operationId: leads_list
      x-cosignal-operation: leads_list
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Leads]
      summary: List your own leads.
      description: >-
        Returns your own workspace's leads, one `LeadSummary` per lead. On
        your own read nothing is withheld: `personName`, `title` and
        `personEmail` are returned when the lead has them. Field values,
        including your chosen custom field values, are on
        `GET /leads/{leadId}` only, and every page says so with
        `record_field_values_on_detail_only`. There is no partner lead read
        in this release. Only `limit` and `cursor` are accepted: an unknown
        or repeated query parameter is `400 invalid_request`. A lead whose
        stored company name is blank is served as `Unnamed company` with
        provenance `sharing` of `absent` for that name.
        `meta.sourceUpdatedAt` is the latest CRM edit time among the leads
        returned and each lead's provenance carries its own (null for the
        owner, which Co-Signal resolves), the page declares
        `no_crm_source_timestamp` only when a returned lead has none,
        `meta.sourceFreshness` states each connected CRM's freshness, and each
        provenance `observedAt` is when Co-Signal last synced the lead. A
        member's own token lists only the
        leads that member owns, the same view the app shows them, so its
        pages depend on that member's own leads alone; an admin created key
        or a signed in admin lists every lead of the workspace.
      security:
        - ecosystemClientCredentials: [ecosystem:leads:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your own leads.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /leads/{leadId}:
    get:
      operationId: leads_detail
      x-cosignal-operation: leads_detail
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Leads]
      summary: Read one of your own leads with its field values.
      description: >-
        One lead with its catalog field values and your workspace's chosen
        `lead.custom.*` values (D-27), each with its own provenance. This is
        the ONLY read in this release that serves chosen custom field values.
        `data.withheld` is always empty on your own read. `leadId` must be a
        `lead_` public id: anything else is `400 invalid_request` on shape
        alone. The route accepts no query parameter at all; any parameter is
        `400 invalid_request`. An id that was never issued, was issued to
        another workspace or names a deleted lead answers the same
        `404 not_found`. `meta.sourceUpdatedAt` is the lead's CRM edit time,
        or null when it has none, and then `limitations` is
        `[no_crm_source_timestamp]` (otherwise it is empty);
        `meta.sourceFreshness` states each connected CRM's freshness. Your own
        chosen field values carry that CRM edit time as `sourceUpdatedAt` and
        the catalog fields a null one, with `observedAt` set to when
        Co-Signal last synced the lead. A member's own token reads only a lead
        that member
        owns, the same view the app shows them, and any other lead answers
        that same `404 not_found`; an admin created key or a signed in admin
        reads any lead of the workspace.
      security:
        - ecosystemClientCredentials: [ecosystem:leads:read]
      parameters:
        - $ref: "#/components/parameters/LeadId"
      responses:
        "200":
          description: The lead and its field values.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /leads/resolve:
    get:
      operationId: leads_resolve
      x-cosignal-operation: leads_resolve
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Leads]
      summary: Find your own leads from a CRM reference, a company domain, a person's name or an email address.
      description: |-
        Takes a CRM reference, a company domain, a person's name or an email
        address as INPUT and returns public ids, which is the supported
        direction for joining a CRM record to a Co-Signal id. Exactly ONE
        selector per request:

        - the pair `source` plus `sourceRecordId` (either half alone names
          nothing), which resolves to every lead carrying that CRM record; a
          Salesforce id is matched in its 15 or 18 character form,
        - `domain`, which resolves to zero or more leads whose company domain
          matches. A personal email domain is refused with
          `400 invalid_request`, because it is never company evidence,
        - `name`, a person's name: an exact match first, then names beginning
          with the same characters, at most ten candidates, or
        - `email`, an email address your own lead holds, matched exactly.

        A token held by a workspace member, rather than an admin or an
        integration key, resolves only the leads that member owns. Any other
        parameter, including `limit` and
        `cursor`, is `400 invalid_request`. An ambiguous selector returns
        several rows rather than a guess. The response is the list envelope
        with `hasMore` false and `nextCursor` null, because a resolve has no
        cursor to continue from; at most 25 rows are returned, and when more
        matched the page carries `results_capped_by_request_budget` with
        `hasMore` still false. A reference that names another workspace's lead
        answers exactly what a reference naming nothing answers. As on the
        lead list, `meta.sourceUpdatedAt` is the latest CRM edit time among
        the leads returned, the answer declares `no_crm_source_timestamp` only
        when a returned lead has none, and `meta.sourceFreshness` states each
        connected CRM's freshness.
      security:
        - ecosystemClientCredentials: [ecosystem:leads:read]
      parameters:
        - name: source
          in: query
          required: false
          description: The CRM the reference comes from. Must be sent together with `sourceRecordId`.
          schema:
            type: string
            enum: [salesforce, hubspot, csv]
        - name: sourceRecordId
          in: query
          required: false
          description: The record id in that CRM. Must be sent together with `source`.
          schema:
            type: string
            minLength: 1
            maxLength: 256
        - name: domain
          in: query
          required: false
          description: A company domain. Never sent together with another selector. A personal email domain is refused.
          schema:
            type: string
            minLength: 1
            maxLength: 253
        - name: name
          in: query
          required: false
          description: >-
            A person's name, compared ignoring case, full stops, commas and
            repeated spaces. An exact match comes first, then names beginning
            with the same characters, at most ten candidates; when more match,
            the page carries `results_capped_by_request_budget`. Never a fuzzy
            or substring match. A name that is empty once compared is refused.
          schema:
            type: string
            minLength: 1
            maxLength: 500
        - name: email
          in: query
          required: false
          description: >-
            An email address one of your own leads holds, matched exactly once
            trimmed and lower cased. A personal address is accepted, because it
            names a person rather than claiming a company. A malformed address
            is refused.
          schema:
            type: string
            minLength: 3
            maxLength: 320
      responses:
        "200":
          description: Zero or more of your own leads, never a guess.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /leads/batch:
    post:
      operationId: leads_batch
      x-cosignal-operation: leads_batch
      x-cosignal-principal-restriction: none
      x-cosignal-availability: released_per_workspace
      tags: [Leads]
      summary: Resolve up to 100 lead selectors in one call.
      description: >-
        The lead equivalent of `POST /accounts/batch`. Takes up to 100
        selectors in a JSON body and answers one item per selector, in the
        order you sent them. Each selector is the same object
        `GET /leads/resolve` takes as its query: a company `domain`, a CRM
        reference (`source` plus `sourceRecordId`, a Salesforce id in its 15 or
        18 character form), a person `name` or an own work `email`, exactly one
        per selector. An item's `candidates` and `limitations` are exactly the
        `data` and `meta.limitations` that `GET /leads/resolve` returns for
        that selector alone, for the same caller, so a batch never reveals
        more than the same selectors resolved one at a time: the same refusal
        of a free mail domain as company evidence, the same name caps, the
        same narrowing to a member's own leads, and the same empty answer for
        a lead that exists only in another workspace as for one that exists
        nowhere.

        An item's `status` is `matched` (one candidate), `ambiguous` (two or
        more, never a guess), `none` or `invalid`; a selector the single
        resolve would refuse with `400 invalid_request` is an `invalid` item
        and the others are still resolved. A body that holds no selector, more
        than 100, a key other than `selectors`, or anything that is not JSON
        is `400 invalid_request` before any selector is read.

        A batch is read only, takes no `Idempotency-Key` and counts once
        against the per minute rate limit. It consumes its selector count from
        the same `batch_selectors_per_day` budget as `POST /accounts/batch`
        (50,000 selectors per workspace per UTC day) before any selector is
        resolved, and a batch that would pass it is refused whole with
        `429 budget_exceeded` and never answered partially.

        Batch resolution is not yet enabled for every workspace: until your
        workspace is released a call answers `503 feature_disabled`. Requires
        `ecosystem:leads:read`.
      security:
        - ecosystemClientCredentials: [ecosystem:leads:read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchResolveRequest"
      responses:
        "200":
          description: One item per selector, in input order.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadBatchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/BatchLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /contacts:
    get:
      operationId: contacts_read
      x-cosignal-operation: contacts_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Contacts]
      summary: List your own contacts.
      description: >-
        Returns your own workspace's contacts, one `ContactSummary` per
        contact, each carrying the `acct_` id of the account it belongs to.
        This is a top level feed only: there are no per account sub resources
        in this release. A contact carries no chosen custom field anywhere.
        `meta.sourceUpdatedAt` is the latest CRM edit time among the
        contacts returned and each row's provenance carries its own, the page
        declares `no_crm_source_timestamp` only when a returned contact has
        none, and `meta.sourceFreshness` states each connected CRM's
        freshness. A contact whose stored name is blank is
        served as `Unknown contact` with provenance `sharing` of `absent` for
        that name. Only `limit` and `cursor` are accepted: an unknown or
        repeated query parameter is `400 invalid_request`. A member's own
        token lists only the contacts of the accounts that member owns or
        claimed, the same view the app shows them, so its pages depend on
        that member's own records alone; an admin created key or a signed
        in admin lists every contact of the workspace.
      security:
        - ecosystemClientCredentials: [ecosystem:contacts:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your own contacts.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /opportunities:
    get:
      operationId: opportunities_read
      x-cosignal-operation: opportunities_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Opportunities]
      summary: List your own opportunities.
      description: >-
        Returns your own workspace's opportunities, one `OpportunitySummary`
        per deal, each carrying the `acct_` id of its account. This is a top
        level feed only. The amount is served exactly as stored, sign
        included. Your chosen opportunity custom field values are stored by
        the sync but are NOT served by any read in this release: when your
        workspace has chosen opportunity fields, the page says so with
        `chosen_field_values_not_served`. `meta.sourceUpdatedAt` is the latest
        CRM edit time among the deals returned and each row's provenance
        carries its own, the page declares `no_crm_source_timestamp` only when
        a returned deal has none, and `meta.sourceFreshness` states each
        connected CRM's freshness. A deal whose stored name is blank is served
        as `Untitled deal` with provenance `sharing` of `absent` for that
        name. Only `limit` and `cursor` are accepted: an unknown or repeated
        query parameter is `400 invalid_request`. A member's own token lists
        only the opportunities of the accounts that member owns or claimed,
        the same view the app shows them, so its pages depend on that
        member's own records alone; an admin created key or a signed in
        admin lists every opportunity of the workspace.
      security:
        - ecosystemClientCredentials: [ecosystem:opportunities:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your own opportunities.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OpportunityListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /partners:
    get:
      operationId: partners_read_list
      x-cosignal-operation: partners_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Partners]
      summary: List your current active partnerships.
      description: >-
        One `PartnerSummary` per current active partnership. Its `id` is the
        `ptsh_` PARTNERSHIP id; the partner company is the nested `workspace`
        with its own `ws_` id. A partnership's existence and the partner's
        name and domain are facts both sides hold, so this feed does not
        depend on any sharing setting. Every key of your workspace sees every
        partnership: there is no per partner key restriction in this release.
        Only `limit` and `cursor` are accepted: an unknown or repeated query
        parameter is `400 invalid_request`.
      security:
        - ecosystemClientCredentials: [ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your partnerships.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /partners/{partnerId}:
    get:
      operationId: partners_read_detail
      x-cosignal-operation: partners_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Partners]
      summary: Read one of your current active partnerships.
      description: >-
        Exactly the summary fields of one partnership, addressed by its
        `ptsh_` id. `partnerId` must be a `ptsh_` public id: anything else is
        `400 invalid_request` on shape alone. The route accepts no query
        parameter at all. An id that was never issued, names another
        workspace's partnership, names a partnership that is no longer active
        or names one you are not a side of answers the same `404 not_found`.
      security:
        - ecosystemClientCredentials: [ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/PartnerId"
      responses:
        "200":
          description: The partnership.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /partner-tags:
    get:
      operationId: partner_tags_read
      x-cosignal-operation: partner_tags_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Partners]
      summary: List your workspace's partner tags.
      description: >-
        The tags your workspace's admins created to label your partnerships,
        each with its `ptag_` id, name and times, ordered by an internal
        stable key rather than alphabetically. Tags are read only through the
        API and are your workspace's own: every member and every key of your
        workspace reads the same list, and the partner you tagged never sees
        them. Each `PartnerSummary` from `GET /partners` carries the tags you
        applied to that partnership under the same ids. Only `limit` and
        `cursor` are accepted: an unknown or repeated query parameter is
        `400 invalid_request`.
      security:
        - ecosystemClientCredentials: [ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your workspace's partner tags.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerTagListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /populations:
    get:
      operationId: populations_read
      x-cosignal-operation: populations_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Populations]
      summary: List your own account and lead populations.
      description: >-
        ONE feed carrying both account and lead populations, discriminated by
        `resourceType`; there is no `?resource=` parameter. Each row is your
        own saved definition, including its `filter`. A delegated caller
        whose effective role is not admin receives an empty page rather than
        a projection; an integration key created by an admin is not a
        delegated caller. Only `limit` and `cursor` are accepted: an unknown
        or repeated query parameter is `400 invalid_request`. `createdAt` and
        `updatedAt` are the stored creation and last edit times; a population
        whose `updatedAt` equals its `createdAt` has never been edited (B-12).
      security:
        - ecosystemClientCredentials: [ecosystem:populations:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your populations.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PopulationListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /populations/{populationId}:
    get:
      operationId: populations_detail
      x-cosignal-operation: populations_detail
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Populations]
      summary: Read one of your own population definitions.
      description: >-
        Exactly the list row of one of your own account or lead populations,
        addressed by the `pop_` id `GET /populations` returns, including its
        `filter`. Like the list, it answers only an admin created key or a
        delegated caller whose effective role is admin. Every other answer is
        the same `404 not_found`: a member's token, an admin previewing as a
        member, a removed member, a malformed id, an id of another kind, an id
        that was never issued, an id issued to another workspace and the id
        of a deleted population cannot be told apart. The route accepts no
        query parameter at all.
      security:
        - ecosystemClientCredentials: [ecosystem:populations:read]
      parameters:
        - $ref: "#/components/parameters/PopulationId"
      responses:
        "200":
          description: The population definition.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PopulationDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /partner-populations:
    get:
      operationId: partner_populations_read_list
      x-cosignal-operation: partner_populations_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Populations]
      summary: List the populations your partners share with you.
      description: >-
        Each population a partner explicitly shares with your workspace, as
        a fixed projection: the name your partner gave it, whether it holds
        accounts or leads, the sharing level your partner granted you, the
        partnership and partner company it arrives through, and how many of
        your own records overlap it. It never carries the partner's
        definition, the population's size, the partner's own population id
        or who changed the share. A partner population with no explicit
        share is never listed. A partner that has not switched on automated
        reads for your partnership contributes nothing to this route, exactly
        as a partner that shares nothing. Like your own population
        definitions, it answers only an admin created key or a delegated
        caller whose effective role is admin; every other caller receives an
        empty page. Rows are ordered by an internal id that is stable but not
        alphabetical. Only `limit` and `cursor` are accepted: an unknown or
        repeated query parameter is `400 invalid_request`.
      security:
        - ecosystemClientCredentials: [ecosystem:populations:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the populations your partners share with you.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPopulationListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /partner-populations/{partnerPopulationId}:
    get:
      operationId: partner_populations_read_detail
      x-cosignal-operation: partner_populations_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Populations]
      summary: Read one population a partner shares with you.
      description: >-
        Exactly the list row of one population a partner shares with you,
        addressed by the `ppop_` id `GET /partner-populations` returns, and
        nothing more: never the partner's definition, the population's size
        or the partner's own population id. Like the list, it answers only an
        admin created key or a delegated caller whose effective role is
        admin. Every other answer is the same `404 not_found`: a member's
        token, an admin previewing as a member, a malformed id, an id of
        another kind, an id that was never issued, an id issued to another
        workspace, a share the partner removed, a partnership that is no
        longer active and a partner that has not switched on automated reads
        cannot be told apart. The route accepts no query parameter at all.
      security:
        - ecosystemClientCredentials: [ecosystem:populations:read]
      parameters:
        - $ref: "#/components/parameters/PartnerPopulationId"
      responses:
        "200":
          description: The shared population.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPopulationDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /overlaps/accounts:
    get:
      operationId: account_overlaps_search
      x-cosignal-operation: account_overlaps_search
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Overlaps]
      summary: List your own accounts with the partnerships that overlap each one.
      description: |-
        One row per account of YOUR OWN, with every partnership that
        currently contributes a qualifying overlap for it nested in
        `partners`. Requires both `ecosystem:accounts:read` and
        `ecosystem:partners:read`.

        **Empty on day one.** A partner's data flows through this API only
        once that partner's owner has switched the automation channel on for
        the partnership, and automation is OFF by default for every
        partnership (D-30). Until an owner does, every row returns an EMPTY
        `partners` array. That is the honest state of the owners' consent,
        not a broken feed. Rows with an empty `partners` array are always
        included, so page boundaries depend on your own accounts alone.

        A member's own token lists only the accounts that member owns or
        claimed, the same view the app shows them, so its pages depend on
        that member's own accounts alone; an admin created key or a signed
        in admin lists every account of the workspace.

        A refused channel, a counts only partner and a partner with no
        sharing rule are indistinguishable here: each is simply absent from
        `partners`, with no error, marker or count difference.

        **Request budget.** One request composes at most 25 accounts. A
        `limit` above 25 returns at most 25 rows; when that budget cut the
        page short and rows remain, the page carries
        `results_capped_by_request_budget` with `hasMore` true, and the cursor
        continues from where it stopped. Overlaps are computed for this
        request (`overlap_recomputed_per_request`), so `computedAt` is the
        computation moment. Only `limit` and `cursor` are accepted. An
        account whose stored display name is blank is served as
        `Unnamed company`. `meta.sourceUpdatedAt` is null and
        `no_crm_source_timestamp` is among the fixed limitations; overlap
        rows carry no provenance of their own.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your accounts with their overlapping partnerships.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountOverlapListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /overlaps/accounts/search:
    get:
      operationId: account_overlaps_query
      x-cosignal-operation: account_overlaps_query
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Overlaps]
      summary: Search your account overlaps by population, partner, tag and shared fields.
      description: |-
        The rows of `GET /overlaps/accounts` narrowed by the filters you
        send. A row is one account of YOUR OWN with at least one partner
        entry that satisfies every filter, and its `partners` holds only
        those entries; accounts with no such entry are not returned.
        Requires both `ecosystem:accounts:read` and
        `ecosystem:partners:read`. A member's own token searches only the
        accounts that member owns or claimed, the same view the app shows
        them; an admin created key or a signed in admin searches every
        account of the workspace.

        Each entry also names the populations the partner shares with you
        that hold this pair (`partnerPopulations`, the `ppop_` ids
        `GET /partner-populations` issues) and carries the fields the
        partner shares with you on this channel (`customFields`, each with
        its provenance). `customFields` is always present: it is empty for
        every reason a field is not shared.

        A search only ever narrows what the overlap feed already shows you:
        filters, predicates and sorts are evaluated over the values a
        partner shares with you, never over anything withheld. A field can
        be used in `where` or `sort` only when the partner named by
        `partner` shares it with you and it is marked filterable, such as
        `account.industry` or `account.employee_count`; any other field
        (withheld, unknown, not filterable such as `account.annual_revenue`,
        a lead field, or chosen by another workspace) is refused exactly
        like an unknown one, with the same `400 invalid_request`. `where`
        and `sort` require `partner`.

        Without `sort`, rows are ordered by your own account's internal
        order. With `sort`, rows are ordered by the field (a row's value is
        the lowest of its entries' values ascending and the highest
        descending), rows without a value come last, and ties keep your own
        account's order. Overlaps are computed for this request
        (`overlap_recomputed_per_request`). A cursor carries only a position
        and the last row's id, never a field value. `meta.sourceUpdatedAt`
        is null and `no_crm_source_timestamp` is among the fixed
        limitations.

        Like the overlap feed, a partner's entries appear only once that
        partner's owner has switched the automation channel on for the
        partnership (OFF by default); a refused channel, a counts only
        partner and a partner with no sharing rule are indistinguishable.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: population
          in: query
          required: false
          description: >-
            A `pop_` id of one of your own account populations: only entries
            whose overlap cell pairs that population with the partner's.
            Admin only: a member's key or token is refused with `400
            invalid_request`, the same answer as an id that does not resolve.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partnerPopulation
          in: query
          required: false
          description: >-
            A `ppop_` id of an incoming shared population: only entries that
            population holds. Admin only, refused for a member exactly like
            an id that does not resolve.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partner
          in: query
          required: false
          description: >-
            A `ptsh_` partnership id: only entries from that partnership.
            Required with `where` or `sort`.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partnerTag
          in: query
          required: false
          description: >-
            A `ptag_` id of one of your workspace's own partner tags: only
            entries from partnerships your workspace tagged with it. A deleted
            tag's id returns an empty page.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: where
          in: query
          required: false
          description: >-
            A JSON encoded array of one to five predicates, each exactly
            `{ "field": string, "operator": string, "value": string or number }`,
            all of which an entry must satisfy. `field` is an account field
            key such as `account.industry` (at most 200 characters);
            `operator` is one of `equals`, `contains`, `greater_than`,
            `less_than`, `before` and `after`; `value` is a string of at most
            4000 characters or a finite number. Text comparisons ignore case;
            `greater_than` and `less_than` compare numbers; `before` and
            `after` compare dates. A value the partner does not share never
            satisfies a predicate. Requires `partner`.
          schema:
            type: string
            minLength: 1
            maxLength: 24000
        - name: sort
          in: query
          required: false
          description: >-
            An account field key to order rows by, prefixed with `-` for
            descending. The same field rule as `where` applies. Requires
            `partner`.
          schema:
            type: string
            minLength: 1
            maxLength: 201
      responses:
        "200":
          description: One page of your accounts with the partner entries that satisfy every filter.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountOverlapSearchResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /overlaps/leads:
    get:
      operationId: lead_overlaps_search
      x-cosignal-operation: lead_overlaps_search
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Overlaps]
      summary: List your own leads with the partner leads and accounts they match.
      description: |-
        One row per lead of YOUR OWN, with every stored match
        that a partner currently shares with you nested in `overlaps`.
        Requires both `ecosystem:leads:read` and `ecosystem:partners:read`.
        A key or token for a member sees only the leads that member owns;
        an integration key and an admin see every lead of the workspace.

        A match appears only when the partner shares, with your workspace,
        a population that contains the partner's matched lead or account at
        the `overlapping` or `all` level, and the partner has switched the
        automation channel on for the partnership (OFF by default). A
        refused channel, a counts only share, no share and a population that
        does not contain the matched record are indistinguishable: the match
        is simply absent from `overlaps`. Rows with an empty `overlaps` array
        are always included, so page boundaries depend on your own leads
        alone.

        `partnerRecord.resourceType` says which kind of partner record
        matched: a partner `lead`, or a partner `account` (your prospect is
        their customer). The partner's person name, title and email appear
        only when the partner shares them. `match` is `same_person` only
        when both you and the partner share person details with each other
        and the work emails match; in every other case it is
        `company_match`, and nothing hints at a possible person match.

        Matches are stored computations, each with its own `computedAt`. One
        request composes at most 50 leads; when that budget cut the page
        short and rows remain, the page carries
        `results_capped_by_request_budget` with `hasMore` true. Only `limit`
        and `cursor` are accepted. `meta.sourceUpdatedAt` is null and
        `no_crm_source_timestamp` is among the fixed limitations.
      security:
        - ecosystemClientCredentials: [ecosystem:leads:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your leads with the partner records they match.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadOverlapListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /overlaps/leads/search:
    get:
      operationId: lead_overlaps_query
      x-cosignal-operation: lead_overlaps_query
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Overlaps]
      summary: Search your lead overlaps by population, partner, tag, match type and shared fields.
      description: |-
        The rows of `GET /overlaps/leads` narrowed by the
        filters you send. A row is one lead of YOUR OWN with at least one
        overlap entry that satisfies every filter, and its `overlaps` holds
        only those entries; leads with no such entry are not returned.
        Requires both `ecosystem:leads:read` and `ecosystem:partners:read`.
        A key or token for a member searches only the leads that member
        owns; an integration key and an admin search every lead of the
        workspace.

        A search only ever narrows what the overlap feed already shows you:
        filters, predicates and sorts are evaluated over the values a
        partner shares with you, never over anything withheld. A field can
        be used in `where` or `sort` only when the partner named by
        `partner` shares it with you and it is marked filterable; any other
        field (withheld, unknown, not filterable, or chosen by another
        workspace) is refused exactly like an unknown one, with the same
        `400 invalid_request`. `where` and `sort` require `partner`.

        Without `sort`, rows are ordered by your own lead's internal order.
        With `sort`, rows are ordered by the field (a row's value is the
        lowest of its entries' values ascending and the highest descending),
        rows without a value come last, and ties keep your own lead's
        order. One request reads at most 5000 stored matches; when that
        budget cut the search short, the page carries
        `results_capped_by_request_budget`. A cursor carries only a position
        and the last row's id, never a field value. `meta.sourceUpdatedAt`
        is null and `no_crm_source_timestamp` is among the fixed
        limitations.
      security:
        - ecosystemClientCredentials: [ecosystem:leads:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: population
          in: query
          required: false
          description: >-
            A `pop_` id of one of your own lead populations: only rows whose
            lead was in that population at the last match computation. Admin
            only: a member's key or token is refused with `400
            invalid_request`, the same answer as an id that does not resolve.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partnerPopulation
          in: query
          required: false
          description: >-
            A `ppop_` id of an incoming shared population: only entries that
            population covers. Admin only, refused for a member exactly like
            an id that does not resolve.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partner
          in: query
          required: false
          description: >-
            A `ptsh_` partnership id: only entries from that partnership.
            Required with `where` or `sort`.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partnerTag
          in: query
          required: false
          description: >-
            A `ptag_` id of one of your workspace's own partner tags: only
            entries from partnerships your workspace tagged with it. A deleted
            tag's id returns an empty page.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: match
          in: query
          required: false
          description: Only entries with this match label.
          schema:
            type: string
            enum: [company_match, same_person]
        - name: matchedType
          in: query
          required: false
          description: Only entries whose matched partner record is of this kind.
          schema:
            type: string
            enum: [lead, account]
        - name: where
          in: query
          required: false
          description: >-
            A JSON encoded array of one to five predicates, each exactly
            `{ "field": string, "operator": string, "value": string or number }`,
            all of which an entry must satisfy. `field` is a field key such as
            `lead.industry` or `account.industry` (at most 200 characters);
            `operator` is one of `equals`, `contains`, `greater_than`,
            `less_than`, `before` and `after`; `value` is a string of at most
            4000 characters or a finite number. Text comparisons ignore case;
            `greater_than` and `less_than` compare numbers; `before` and
            `after` compare dates. A value the partner does not share never
            satisfies a predicate. Requires `partner`.
          schema:
            type: string
            minLength: 1
            maxLength: 24000
        - name: sort
          in: query
          required: false
          description: >-
            A field key to order rows by, prefixed with `-` for descending.
            The same field rule as `where` applies. Requires `partner`.
          schema:
            type: string
            minLength: 1
            maxLength: 201
      responses:
        "200":
          description: One page of your leads with the partner records they match that satisfy every filter.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadOverlapListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /reports:
    get:
      operationId: reports_read_list
      x-cosignal-operation: reports_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Reports]
      summary: List the saved reports you may see.
      description: >-
        Each row is one saved report: its name, its view, the partnership it
        reads and whether it is shared with every member of your workspace.
        A report stores its definition only, never a row; its rows are on
        `GET /reports/{reportId}/rows`. An integration key sees only the
        reports shared with the workspace, because a key has no creator; a
        person signed in through an app sees their own reports and the shared
        ones, and `createdByYou` is true only on their own. Two reports with
        the same name are both listed. The order is the lower cased name,
        then the creation time, then a stable internal order, identical on
        every repeat. A workspace with no report you may see answers an empty
        page. Only `limit` and `cursor` are accepted: an unknown or repeated
        query parameter is `400 invalid_request`. Reports are created,
        renamed, shared and deleted in the app.
      security:
        - ecosystemClientCredentials: [ecosystem:reports:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the reports you may see.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /reports/{reportId}:
    get:
      operationId: reports_read_detail
      x-cosignal-operation: reports_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Reports]
      summary: Read one saved report's definition.
      description: >-
        The list row of one report plus its saved definition: the
        partnership, the optional population, shared population and partner
        tag filters, at most five predicates, the sort and the columns. Every
        id in the definition is a public id issued to your own workspace
        (`ptsh_`, `pop_`, `ppop_`, `ptag_`); a filter the report does not set
        is null. Never a row. A report hidden from you (another person's
        private report), a malformed id, an id of another kind, an id that
        was never issued and an id issued to another workspace all answer the
        same `404 not_found`. The route accepts no query parameter at all.
      security:
        - ecosystemClientCredentials: [ecosystem:reports:read]
      parameters:
        - $ref: "#/components/parameters/ReportId"
      responses:
        "200":
          description: The report and its definition.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /reports/{reportId}/rows:
    get:
      operationId: reports_read_rows
      x-cosignal-operation: reports_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Reports]
      summary: Read one page of a saved report's rows, recomputed now.
      description: |-
        The report's rows, recomputed under today's sharing on every call; a
        report never returns rows saved from an earlier moment. An accounts
        report's rows are exactly the rows `GET /overlaps/accounts/search`
        returns for the same filters, projected to the report's columns: one
        row per account of your own, carrying every matching partner entry.
        Rows are ordered by the report's sort with absent values last, then a
        stable internal order; an unsorted report uses the stable order.

        Rows are computed for the caller who asks: a member's own token sees
        only the accounts that member owns or claimed, so a shared report
        never shows a member a row their own search would not show.

        API reads follow each partner's channel settings: a partner who has
        not switched on automation for your partnership contributes no rows
        over the API, while the same report in the app shows them. Every page
        carries the limitation `partner_channel_settings_apply`, whether or
        not anything was withheld. A report that matches nothing answers an
        empty page. The same `404 not_found` as the detail route answers any
        report you may not see. Only `limit` and `cursor` are accepted.
      security:
        - ecosystemClientCredentials: [ecosystem:reports:read]
      parameters:
        - $ref: "#/components/parameters/ReportId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the report's rows.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportRowsResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /lists:
    get:
      operationId: lists_detail_read_list
      x-cosignal-operation: lists_detail_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Lists]
      summary: List the lists you may see.
      description: >-
        Each row is one list: its name, whether it is shared with every member
        of your workspace, and when it was created and last updated. A list
        stores its filters only, never a row; its filters and link are on
        `GET /lists/{listId}` and its rows on `GET /lists/{listId}/rows`. An
        integration key and an admin see every list of the workspace; a member
        signed in through an app sees the lists shared with members. Two lists
        with the same name are both listed. The order is the lower cased name,
        then a stable internal order, identical on every repeat. A workspace
        with no list you may see answers an empty page. Only `limit` and
        `cursor` are accepted: an unknown or repeated query parameter is
        `400 invalid_request`. Lists are created, changed and deleted in the
        app, or with `POST /lists`, `PATCH /lists/{listId}` and
        `DELETE /lists/{listId}`.
      security:
        - ecosystemClientCredentials: [ecosystem:lists:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the lists you may see.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: lists_create
      x-cosignal-operation: lists_create
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Lists]
      summary: Create a list.
      description: >-
        Creates a list with a name of 1 to 60 characters (trimmed) and
        optional filters: `pop_` ids of your own populations, `ptsh_` ids of
        your partnerships and relationship types. An omitted filter is no
        filter. The rules are the app's: at most 50 lists per workspace, and
        every filter id must be your own; anything else is `400
        invalid_request`. An integration key, or a workspace admin signed in
        through an app, may create lists; a member is refused exactly as a
        missing scope is. Send an `Idempotency-Key`: a retry with the same key
        and the same body answers the first `201` with the list as it is now
        and creates nothing, and the same key with another body is `422
        idempotency_key_reused`. The list is attributed to your key or to you
        and the change is recorded. Answers `201` with the list and its
        version as the `ETag`.
      security:
        - ecosystemClientCredentials: [ecosystem:lists:write]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListCreateRequest"
      responses:
        "201":
          description: The new list, version 1.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /lists/{listId}:
    get:
      operationId: lists_detail_read_detail
      x-cosignal-operation: lists_detail_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Lists]
      summary: Read one list's filters and its link.
      description: >-
        The index row of one list plus its saved filters and its `link`. Every
        id in the filters is a public id issued to your own workspace (`pop_`
        for your populations, `ptsh_` for partnerships); an empty filter means
        no filter on that dimension. `link` is the list's authenticated
        Co-Signal link: it opens the list in the app only for a signed in
        member of your workspace who may see it, checked when it is opened,
        and it is never a public, anonymous or signed link. Never a row. A
        list you may not see (a list not shared with members, for a member),
        a malformed id, an id of another kind, an id that was never issued
        and an id issued to another workspace all answer the same
        `404 not_found`. The route accepts no query parameter at all.
      security:
        - ecosystemClientCredentials: [ecosystem:lists:read]
      parameters:
        - $ref: "#/components/parameters/ListId"
      responses:
        "200":
          description: The list, its filters, its link and its version.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
    patch:
      operationId: lists_update
      x-cosignal-operation: lists_update
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Lists]
      summary: Change a list.
      description: >-
        Changes any of a list's `name`, `filters` (replaced whole) and
        `shared`, with the same rules as creating one. Send the version you
        read as `If-Match`, quoted as the `ETag` gives it: without it the
        answer is `428 precondition_required`, and if the list has changed
        since, `409 version_conflict` with nothing written, so read it again
        first. Send an `Idempotency-Key`: a retry with the same key and the
        same request answers the first result. An id naming no list of your
        workspace is the one `404 not_found`. An integration key or a
        workspace admin may change lists; a member is refused exactly as a
        missing scope is. Answers `200` with the list and its new version as
        the `ETag`.
      security:
        - ecosystemClientCredentials: [ecosystem:lists:write]
      parameters:
        - $ref: "#/components/parameters/ListId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListUpdateRequest"
      responses:
        "200":
          description: The changed list and its new version.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
            ETag:
              $ref: "#/components/headers/ETag"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListDetailResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "428":
          $ref: "#/components/responses/PreconditionRequired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
    delete:
      operationId: lists_delete
      x-cosignal-operation: lists_delete
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Lists]
      summary: Delete a list.
      description: >-
        Deletes a list. A list holds no rows, so nothing else is removed. The
        same `If-Match` and `Idempotency-Key` rules as changing one apply:
        `428` without the version, `409` when it is stale. An id naming no
        list of your workspace is the one `404 not_found`. Answers `204` with
        no body.
      security:
        - ecosystemClientCredentials: [ecosystem:lists:write]
      parameters:
        - $ref: "#/components/parameters/ListId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      responses:
        "204":
          description: The list was deleted.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/VersionConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "428":
          $ref: "#/components/responses/PreconditionRequired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /lists/{listId}/rows:
    get:
      operationId: lists_detail_read_rows
      x-cosignal-operation: lists_detail_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Lists]
      summary: Read one page of a list's rows, recomputed now.
      description: |-
        The list's rows, recomputed under today's sharing on every call, by
        the same computation the list page in the app runs; a list never
        returns rows saved from an earlier moment. Each row is one account of
        your own with the partners that place it in each relationship column:
        `customerOf`, `prospectOf` and `openOpportunityFor`. An account no
        partner places in any column is not a row. Rows are ordered by account
        name, then a stable internal order, identical on every repeat; a page
        boundary carries a position, never a row value.

        Rows are computed for the caller who asks: a member's own token sees
        only the accounts that member owns or claimed, and a member who owns
        none receives an empty page.

        API reads follow each partner's channel settings: a partner who has
        not switched on automation for your partnership places no account in
        any column over the API, while the same list in the app shows it.
        Every page carries the limitation `partner_channel_settings_apply`,
        whether or not anything was withheld. A list whose filters match
        nothing answers an empty page. The same `404 not_found` as the detail
        route answers any list you may not see. A cursor is bound to its list
        and its filters: after the list's filters change it is
        `400 cursor_query_mismatch`. Only `limit` and `cursor` are accepted.
      security:
        - ecosystemClientCredentials: [ecosystem:lists:read]
      parameters:
        - $ref: "#/components/parameters/ListId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the list's rows.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListRowsResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /exports:
    post:
      operationId: exports_read_create
      x-cosignal-operation: exports_read
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Exports]
      summary: Create an export of your own records.
      description: >-
        Starts an asynchronous export of the families you name and answers
        `202` with the export's status (`queued`, its `job_` id) and the
        `actionId` of the action that follows it. The export is built in the
        background; read `GET /exports/{exportId}` until it is `complete`,
        then download its parts with `GET /exports/{exportId}/download`.
        `families` is any of `accounts`, `contacts`, `opportunities`, `leads`,
        `account_overlaps` and `lead_overlaps`, each once. `format` is `ndjson`
        (the default: one `{ "family", "data" }` row per line) or `csv`, which
        carries exactly one family. A partner's data appears only if that
        partner allows the export channel, which is off by default for every
        partnership.

        **Who can call it.** Exports are an organization wide power
        (`admin_integration_only`): a key that holds `ecosystem:exports:read`,
        or a workspace admin through an app the workspace approved for it.
        Every other caller, a member included, is refused `403
        insufficient_scope` exactly as a caller without the scope is. The
        scope alone is not enough: the caller also needs the scopes that read
        each family it asks for (for example `ecosystem:accounts:read` for
        `accounts`), and one family out of reach refuses the whole request
        with the same `403 insufficient_scope`, before anything is written.

        **Idempotency.** `Idempotency-Key` is required: a missing or malformed
        key is `400 invalid_request` (never `428`). The same key with the same
        body answers `202` with the same export id and `actionId` and the
        export's current status (which may already be `running` or
        `complete`), and creates nothing more; a replay after the export's
        record has been deleted at the end of its retention answers `404
        not_found`. The same key with another body is
        `422 idempotency_key_reused` and writes nothing.
        A body with an unknown key, an unknown or repeated family, an empty
        list, a `csv` export of more than one family, or anything that is not
        JSON is `400 invalid_request`, before anything is written.

        **Budgets.** A workspace can have 3 exports open at once
        (`export_concurrent_jobs`): a fourth is refused whole with `429
        budget_exceeded`, a `resetsAt` of null and a `Retry-After` of 60
        seconds, before anything is written. Rows are counted against 1,000,000
        rows per workspace per UTC day (`export_rows_per_day`) as parts are
        written: an export whose next part would pass the day's rows ends
        `failed` with the budget named in its status, never silently cut short.

        The action's receipt, read at `GET /actions/{actionId}` by a key that
        also holds a command scope, ends `succeeded` (with counts) when the
        export completes and stays `succeeded` after the download window
        closes; `failed` for a real failure; and `refused` for both a
        cancelled and a revoked export, so it never shows which of the two
        happened. Bulk exports are not yet enabled for every workspace: until
        your workspace is released a call answers `503 feature_disabled`,
        byte for byte the answer of a workspace outside the API rollout.
        Requires `ecosystem:exports:read`.
      security:
        - ecosystemClientCredentials: [ecosystem:exports:read]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExportCreateRequest"
      responses:
        "202":
          description: The export was queued (or the stored answer of an earlier call with the same key and body).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportCreatedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/ExportLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /exports/{exportId}:
    get:
      operationId: exports_read_status
      x-cosignal-operation: exports_read
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Exports]
      summary: Read the status of one of your exports.
      description: >-
        The status of an export, for the key or person that created it only.
        It is a projection of the export and nothing else: the `job_` id, the
        `status` (`queued`, `running`, `complete`, `failed`, `cancelled`,
        `revoked` or `expired`), the families, the format, the snapshot
        `watermark`, the row counts and, for a `complete` export, its parts
        (number, family, row count, byte size, sha256 and content type; never
        a storage path or a link) and `expiresAt`, the end of the 24 hour
        download window. Export files are kept for 24 hours after the export
        completes; after that the export reads `expired` and lists no part.

        A cancelled, revoked, failed or expired export lists no part and no
        bookmark. A revoked export says only `revoked`: it gives no reason. A
        failed export carries `error` with a closed `code` (`budget_exceeded`,
        with the budget named, `export_too_large` or `internal`). A complete
        export carries one
        change feed bookmark for each family in `bookmarks`: pass it as
        `bookmark` to `GET /changes` with that family as `resource` to receive
        what changed after the export, without a new snapshot.

        Another key of the workspace, another workspace's key, an id that was
        never issued and a malformed id are each answered the same `404
        not_found`. The route takes no query parameter. Not yet enabled for
        every workspace: until yours is released a call answers `503
        feature_disabled`. Requires `ecosystem:exports:read`.
      security:
        - ecosystemClientCredentials: [ecosystem:exports:read]
      parameters:
        - $ref: "#/components/parameters/ExportId"
      responses:
        "200":
          description: The export's status.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /exports/{exportId}/cancel:
    post:
      operationId: exports_read_cancel
      x-cosignal-operation: exports_read
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Exports]
      summary: Ask for the end of one of your exports.
      description: >-
        Cancels your own export and answers the status you would read next.
        Cancelling a `queued` or `running` export is a request: it is recorded,
        the answer still reads `queued` or `running`, and the worker ends the
        export `cancelled` at its next step (before its next page). A
        `complete` export inside its download window is cancelled at once and
        its parts are deleted. Any other state, and a complete export whose
        window has passed, is unchanged. Cancelling twice is the same as once.
        A cancelled export lists no part, and its action ends `refused`, the
        same receipt a revoked export gets.

        Another key of the workspace, another workspace's key, an id that was
        never issued and a malformed id are each answered the same `404
        not_found`, and a stranger's cancel changes nothing. The route takes no
        body, no query parameter and no `Idempotency-Key` (cancelling is
        idempotent by nature). Not yet enabled for every workspace: until yours
        is released a call answers `503 feature_disabled`. Requires
        `ecosystem:exports:read`.
      security:
        - ecosystemClientCredentials: [ecosystem:exports:read]
      parameters:
        - $ref: "#/components/parameters/ExportId"
      responses:
        "200":
          description: The export's status after the cancel (a request for a running export, the end for a complete one).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /exports/{exportId}/download:
    get:
      operationId: exports_read_download
      x-cosignal-operation: exports_read
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Exports]
      summary: Download one part of a complete export.
      description: >-
        Streams one part of a `complete` export through this authenticated
        call. No storage link, path or bucket name is ever returned and nothing
        redirects to storage. The response carries `Content-Type`
        (`application/x-ndjson`, or `text/csv`), `Content-Disposition:
        attachment`, `Cache-Control: no-store` and an `ETag` of the part's
        sha256. A part holds at most 5,000 rows or 8 MiB, and a family can be
        several parts.

        An NDJSON part has one line per row (`ExportLine`, discriminated by
        `family`, and `data` is always the row of that family). A line of
        `accounts`, `contacts`, `opportunities` or `leads` is
        `{ "family", "data" }`, and a line of `account_overlaps` or
        `lead_overlaps` is `{ "family", "id", "data" }`, where `id` is the same opaque id
        `GET /changes` gives that pair, so a tombstone from the change feed
        can be matched to the exported row of its pair. A CSV part of an
        overlap family leads with an `id` column holding the same id.

        Export files are kept for 24 hours after the export completes.
        Download them within that time, or run the export again. Every part is
        authorized again when it is downloaded, with the caller's rights at
        that moment: the creator's key or app approval must still be live and
        must still hold the scopes of every exported family, and no partner may
        have changed its sharing since the export's snapshot (any change,
        loosening and a `crm_sync` toggle included, ends the export for good;
        a running export ends at its next page and a completed one at its next
        download). Parts already downloaded cannot be recalled.

        Every refusal is the same `404 not_found`: another key of the
        workspace, another workspace's key, an id never issued, a part that
        does not exist, an expired export, a cancelled, revoked or failed
        export, and a part the storage can no longer read before its first
        byte. A part that fails after its first byte aborts the transfer
        instead of ending as a short file, and the body never delivers a byte
        count or sha256 other than the one in the status. `part` is judged on
        the request alone: it must be given once, as a positive whole number,
        and any other query parameter is `400 invalid_request`. Not yet enabled
        for every workspace: until yours is released a call answers `503
        feature_disabled`. Requires `ecosystem:exports:read`.
      security:
        - ecosystemClientCredentials: [ecosystem:exports:read]
      parameters:
        - $ref: "#/components/parameters/ExportId"
        - $ref: "#/components/parameters/ExportPartNumber"
      responses:
        "200":
          description: The part's bytes.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
            Content-Disposition:
              description: '`attachment; filename="export-<id>-part-<n>.ndjson"` (or `.csv`).'
              schema:
                type: string
            ETag:
              description: The part's sha256 in hex, quoted.
              schema:
                type: string
                pattern: '^"[0-9a-f]{64}"$'
          content:
            application/x-ndjson:
              schema:
                type: string
                format: binary
            text/csv:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /imports:
    post:
      operationId: imports_write_create
      x-cosignal-operation: imports_write
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Imports]
      summary: Upload a CSV of accounts and start a dry run.
      description: >-
        Stores your CSV and queues a dry run, and answers `202` with the
        import's status (`validating`, its `job_` id) and the `actionId` of the
        action that follows it. The dry run reads every row and writes no
        account. Read `GET /imports/{importId}` until it is `dry_run_complete`,
        then start the apply with `POST /imports/{importId}/apply`.

        **The body** is `multipart/form-data` with exactly two fields: `file`,
        the CSV (up to 25 MiB and 100,000 data rows), and `options`, JSON
        text. `options.mapping` names your columns (`name` and `domain` are
        required; `type`, `ownerEmail` and `crmRecordId` are optional, and
        `crmRecordId` needs a `crmSource` of `salesforce` or `hubspot`) and
        `options.precedence.importWinsFields` optionally names `name` and
        `type`, the fields the import should win for. A file over 25 MiB, a
        body that is not multipart, a field other than the two, a missing or
        repeated field, options that fail the schema, an empty file and a file
        that does not look like a CSV are `400 invalid_request`, and no import is
        created. The ecosystem error vocabulary is closed and has no
        payload too large code, so the limit is stated here. The file name can
        be up to 255 characters and `options` up to 16 KiB, and an instance
        already reading two uploads answers a third `429 rate_limited` with a
        `Retry-After`, which you can send again.

        **Who can call it.** Imports are an organization wide power
        (`admin_integration_only`): a key that holds `ecosystem:imports:write`,
        or a workspace admin through an app the workspace approved for it.
        Every other caller, a member included, is refused `403
        insufficient_scope` exactly as a caller without the scope is.

        **Idempotency.** `Idempotency-Key` is required: a missing or malformed
        key is `400 invalid_request` (never `428`). The same key with the same
        file and options answers `202` with the same import id and `actionId`
        and the import's current status (which may already be
        `dry_run_complete`), and creates nothing more; a replay after the
        import's record has been deleted at the end of its retention answers
        `404 not_found`. The same key with another file or other options is
        `422 idempotency_key_reused` and creates no import.

        **Limits.** A workspace can have 2 imports open at once, counting
        those that are `validating` or `applying` (`import_concurrent_jobs`): a
        third is refused whole with `429 budget_exceeded`, a `resetsAt` of null
        and a `Retry-After` of 60 seconds, and no import is created. A file
        with more than 100,000 data rows ends the dry run `failed` with the
        closed code `too_many_rows`, a file the parser cannot read ends it
        `failed` with `invalid_file`, a stored file that has gone missing ends
        it `failed` at once with `file_unavailable`, and an unexpected failure
        ends it with `internal`.

        The create action's receipt, read at `GET /actions/{actionId}` by a key
        that also holds a command scope, ends `succeeded` (with the dry run's
        counts) when the dry run completes; `failed` for a real failure; and
        `refused` for both a cancelled and a revoked import, so it never shows
        which of the two happened. Bulk imports are not yet enabled for every
        workspace: until your workspace is released a call answers `503
        feature_disabled`, byte for byte the answer of a workspace outside the
        API rollout. Requires `ecosystem:imports:write`.
      security:
        - ecosystemClientCredentials: [ecosystem:imports:write]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ImportUploadRequest"
            encoding:
              options:
                contentType: application/json
      responses:
        "202":
          description: The dry run was queued (or the stored answer of an earlier call with the same key and body).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportCreatedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/ImportLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /imports/{importId}:
    get:
      operationId: imports_write_status
      x-cosignal-operation: imports_write
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Imports]
      summary: Read the status and row report of one of your imports.
      description: >-
        The status of an import, for the key or person that created it only.
        It is a projection of the import and nothing else: the `job_` id, the
        `status` (`validating`, `dry_run_complete`, `applying`, `applied`,
        `failed`, `cancelled`, `revoked` or `expired`), the file's name, the
        times, the dry run's report once it finished and the apply's report so
        far. A report is exact counts (`total`, `creates`, `updates`, `skips`,
        `conflicts` and `errors`), up to 200 row errors as `{ row, field,
        code }` and up to 200 conflicts as `{ row, fields }`. A report names
        rows and fields and never a cell value, a company name, a domain, an
        account id or a storage path. A repeat of an earlier row of the same
        file is counted as a skip and listed with `duplicate_in_file`.

        The row error codes are `missing_name`, `invalid_domain`,
        `personal_email_domain`, `invalid_type`, `invalid_owner_email`,
        `missing_identity`, `duplicate_in_file`, `ambiguous_crm_reference`,
        `identity_mismatch` and `value_too_long`, and a code is never renamed
        or removed. A failed import carries `error` with its closed `code`
        (`too_many_rows`, `invalid_file`, `file_unavailable` or `internal`); a cancelled or revoked
        import says nothing beyond its status. `expiresAt` is when the file of
        an unapplied dry run expires: it is deleted 7 days after the dry run
        finished and the import then reads `expired`.

        Status, cancel and apply answer only the creator. Another key of the
        workspace, another workspace's key, an id that was never issued and a
        malformed id are each answered the same `404 not_found`. The route
        takes no query parameter. Not yet enabled for every workspace: until
        yours is released a call answers `503 feature_disabled`. Requires
        `ecosystem:imports:write`.
      security:
        - ecosystemClientCredentials: [ecosystem:imports:write]
      parameters:
        - $ref: "#/components/parameters/ImportId"
      responses:
        "200":
          description: The import's status and reports.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /imports/{importId}/apply:
    post:
      operationId: imports_write_apply
      x-cosignal-operation: imports_write
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Imports]
      summary: Apply a finished dry run of one of your imports.
      description: >-
        Starts the apply of an import whose dry run is `dry_run_complete`, for
        its creator only, and answers `202` with the import's status
        (`applying`) and the apply's own `actionId`. A dry run always comes
        first. The route reads no body and takes no query parameter, and it
        needs its own `Idempotency-Key`: the create's key is spent. An import
        in any other state (still `validating`, already `applying` or
        `applied`, `failed`, `cancelled`, `revoked` or `expired`), and an import
        with a cancel request recorded, is `400 invalid_request` and starts
        nothing.

        **Semantics.** The apply checks every row again against the data as it
        is when the apply reaches it, so a value filled in since the dry run is
        skipped, not overwritten. By default the CRM wins and the import fills
        gaps: a name or type already on an account is never overwritten (unless
        `precedence.importWinsFields` named that field at create), a blank cell
        never clears a value, an owner column only fills an empty owner, and an
        import never deletes anything and writes accounts only. A row is
        matched to an account by its domain, or by its CRM record id when you
        map one, and a new account needs a domain. Every applied row is audited
        with the name of the key (or person) that applied it.

        **Budgets.** Applies are limited to 200,000 rows per workspace per UTC
        day (`import_rows_per_day`), and an apply counts every row of its dry
        run that passed validation. An apply that would pass it is `429
        budget_exceeded` with the budget's `resetsAt`, and so is an apply
        started while 2 other imports are open (`import_concurrent_jobs`, a
        `resetsAt` of null), the import stays `dry_run_complete`, and nothing
        is applied or charged.

        The apply's receipt, read at `GET /actions/{actionId}` by a key that
        also holds a command scope, ends `succeeded` (with the apply's counts)
        when the import is `applied`; `failed` for a real failure; and `refused`
        for both a cancelled and a revoked import. A replay with the same key
        answers `202` with the same `actionId` and the import's current status,
        and applies nothing more.

        Another key of the workspace, another workspace's key, an id that was
        never issued and a malformed id are each answered the same `404
        not_found`, and a stranger's apply changes nothing. Not yet enabled for
        every workspace: until yours is released a call answers `503
        feature_disabled` and starts nothing. Requires
        `ecosystem:imports:write`.
      security:
        - ecosystemClientCredentials: [ecosystem:imports:write]
      parameters:
        - $ref: "#/components/parameters/ImportId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          description: The apply was started (or the stored answer of an earlier call with the same key).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportCreatedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/ImportLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /imports/{importId}/cancel:
    post:
      operationId: imports_write_cancel
      x-cosignal-operation: imports_write
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Imports]
      summary: Ask for the end of one of your imports.
      description: >-
        Cancels your own import and answers the status you would read next.
        Cancelling a `validating` or `applying` import is a request: it is
        recorded, the answer still reads the running status, and the import
        ends `cancelled` before its next chunk of 300 rows. Rows an apply had
        already applied stay applied and audited, and a chunk that had already
        started when the import was cancelled or revoked can still finish, so
        up to 300 rows, each audited, can be applied after it. An apply started
        after a cancel was requested is refused. A finished dry run is
        cancelled at once and its stored file is deleted. Any other state is
        unchanged, and cancelling twice is the same as once. A cancelled import
        ends the action that was still open `refused` with the closed code
        `job_ended`, the same ending a revoked import gets.

        Another key of the workspace, another workspace's key, an id that was
        never issued and a malformed id are each answered the same `404
        not_found`, and a stranger's cancel changes nothing. The route takes no
        body, no query parameter and no `Idempotency-Key` (cancelling is
        idempotent by nature). Not yet enabled for every workspace: until yours
        is released a call answers `503 feature_disabled` and changes nothing.
        Requires `ecosystem:imports:write`.
      security:
        - ecosystemClientCredentials: [ecosystem:imports:write]
      parameters:
        - $ref: "#/components/parameters/ImportId"
      responses:
        "200":
          description: The import's status after the cancel (a request for a running import, the end for a finished dry run).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /webhook-subscriptions:
    get:
      operationId: webhooks_manage_list
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: List your webhook subscriptions.
      description: >-
        The workspace's webhook subscriptions, newest first, 25 a page by
        default. Each row is a `WebhookSubscription`: its URL, event types,
        partnership filter, `status`, the public `statusReason` and the ids of
        the keys that sign its deliveries. A row never carries the signing
        secret (shown once, when it is made) or the challenge token, and a
        deleted subscription is not listed. Only `limit` and `cursor` are
        accepted: an unknown or repeated query parameter is `400
        invalid_request`.

        **Who can call it.** Webhook management is an organization wide power
        (`admin_integration_only`): a key that holds
        `ecosystem:webhooks:manage`, or a workspace admin through an app the
        workspace approved for it. Every other caller, a member included, is
        refused `403 insufficient_scope` exactly as a caller without the scope
        is. Webhooks are not yet enabled for every workspace: until yours is
        released a call answers `503 feature_disabled`, byte for byte the
        answer of a workspace outside the API rollout. Requires
        `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of the workspace's webhook subscriptions.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscriptionListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: webhooks_manage_create
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Register an endpoint that Co-Signal sends signed events to.
      description: >-
        Creates a webhook subscription. Send the endpoint's `url` (HTTPS on
        port 443, no user name or password in it, a public host: any other
        form is `400 invalid_request` with no detail, so a refusal never says
        which rule refused it), the `eventTypes` to receive (each once; see
        `WebhookEventEnvelope` for the catalogue) and, optionally,
        `filters.partnershipIds` to narrow it to some of your partnerships
        (`ptsh_` ids; none means every partnership).

        **The handshake.** A new subscription is `pending_verification`.
        Co-Signal sends ONE `GET` to your URL with a `cosignal_challenge`
        query parameter appended (with `?`, or `&` when the URL already has a
        query), the header `Accept: text/plain, application/json`, no body, no
        signature and no event data. The subscription becomes `active` when
        the endpoint answers 2xx with the token as the whole plain text body
        (surrounding whitespace is ignored) or as the JSON object
        `{ "challenge": "<token>" }`. There are up to three attempts: the first
        right away, the second 5 seconds after the first fails and the third
        15 seconds after the second fails. A redirect is never followed, and a
        body cut at the size limit never counts. After the third failure the
        subscription reads `verification_failed` and says nothing more about
        why. A new URL, and a reactivation, start the handshake again.

        **The secret.** The answer carries the signing secret in `secret`
        once. Store it now: no read, update or list returns it again, and
        rotating it makes a new one. Deliveries are signed with it as
        described in the guide at https://co-signal.com/developers#webhooks.

        **Who can call it, and with which scopes.** The same organization wide
        power as every webhook route (`admin_integration_only`). The key also
        needs the scopes of every event type it asks for, because deliveries
        are authorized with them: `ecosystem:signals:read`,
        `ecosystem:accounts:read` and `ecosystem:partners:read` for the
        overlap, request and partner deal types, `ecosystem:signals:read` and
        `ecosystem:opportunities:read` for the own deal types,
        `ecosystem:signals:read` and `ecosystem:accounts:read` for
        `data_health.changed`, and `ecosystem:signals:read`,
        `ecosystem:opportunities:read`, `ecosystem:attribution:read` and
        `ecosystem:partners:read` for `attribution.updated` (the one event
        that carries partner names and a deal value, see
        `AttributionEventData`). A type the key lacks a scope for refuses the
        whole request with the same `403 insufficient_scope`, before anything
        is written.

        **Idempotency.** `Idempotency-Key` is required: a missing or malformed
        key is `400 invalid_request`. The same key with the same body answers
        `201` again with the subscription as it is now and WITHOUT the secret,
        and creates nothing more; the same key with another body is `422
        idempotency_key_reused`. A URL the workspace already has a
        subscription for, and the eleventh subscription (a workspace may hold
        ten), are `409 subscription_conflict`. A body with an unknown key, an
        unknown event type, `webhook.test`, an empty type list, or anything
        that is not JSON is `400 invalid_request`.

        Webhooks are not yet enabled for every workspace: until yours is
        released a call answers `503 feature_disabled`, byte for byte the
        answer of a workspace outside the API rollout. Requires
        `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookSubscriptionCreate"
      responses:
        "201":
          description: The new subscription, `pending_verification`, with its signing secret (absent on a replay of the same key and body).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSecret"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/SubscriptionConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /webhook-subscriptions/{subscriptionId}:
    get:
      operationId: webhooks_manage_read
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Read one webhook subscription.
      description: >-
        One subscription as a `WebhookSubscription`: never the signing secret
        and never the challenge token. A subscription whose key or app was
        revoked, or that no longer has the scopes of its event types, reads
        `suspended` with a `statusReason` naming the public cause. Another
        workspace's id, a deleted subscription, an id that was never issued,
        a malformed id and an id of another type all answer the same `404
        not_found`. The route accepts no query parameter. Not yet enabled for
        every workspace: until yours is released a call answers `503
        feature_disabled`. Requires `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/SubscriptionId"
      responses:
        "200":
          description: The subscription.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscriptionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
    patch:
      operationId: webhooks_manage_update
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Change, disable or reactivate a webhook subscription.
      description: >-
        Changes any of `url`, `eventTypes` and `filters`, or sets `status` to
        `disabled` (deliveries stop and the subscription reads `disabled`,
        with the reason `disabled_by_owner`) or `active` (a reactivation).
        Send at least one field. A new URL, and a reactivation of a disabled,
        suspended or failed subscription, or of one still pending
        verification, start the handshake again, so the answer reads
        `pending_verification`; a change of types or filters
        alone keeps the status. The key needs the scopes of every type the
        subscription ends up with, as on create: adding `attribution.updated`
        needs `ecosystem:attribution:read` and `ecosystem:partners:read` among
        them, and without them the whole request is the same `403
        insufficient_scope` and nothing changes. A URL another subscription
        of the workspace already uses is `409 subscription_conflict`. A body
        with an unknown key, an unknown event type, `webhook.test`, a status
        other than `disabled` or `active`, or anything that is not JSON is
        `400 invalid_request`. Another workspace's id, a deleted subscription
        and an id that was never issued answer the same `404 not_found`. The
        route takes no query parameter and no `Idempotency-Key`. Not yet
        enabled for every workspace: until yours is released a call answers
        `503 feature_disabled`. Requires `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/SubscriptionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookSubscriptionUpdate"
      responses:
        "200":
          description: The changed subscription.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSubscriptionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/SubscriptionConflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
    delete:
      operationId: webhooks_manage_delete
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Delete a webhook subscription.
      description: >-
        Deletes the subscription: it stops receiving events, its signing keys
        are retired, and it reads `404 not_found` from then on. Events already
        sent cannot be recalled. Another workspace's id, a deleted
        subscription and an id that was never issued answer the same `404
        not_found`. The route takes no body, no query parameter and no
        `Idempotency-Key`. Answers `204` with no body. Not yet enabled for
        every workspace: until yours is released a call answers `503
        feature_disabled`. Requires `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/SubscriptionId"
      responses:
        "204":
          description: The subscription was deleted.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /webhook-subscriptions/{subscriptionId}/test:
    post:
      operationId: webhooks_manage_test
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Send one signed test event to an active subscription.
      description: >-
        Sends one `webhook.test` event, signed and delivered exactly like a
        real one, to an `active` subscription, and answers what the endpoint
        did: `delivered` is true exactly when it answered 2xx, and otherwise
        `outcome` is the public outcome of the one attempt (the status code it
        answered, or `timeout`, `tls_error` or `unreachable`). Nothing else
        about the attempt is returned: never a reason, a header or a body. A
        test call is never retried. A subscription that is not `active`
        (pending, failed, suspended or disabled), another workspace's id, a
        deleted subscription and an id that was never issued answer the same
        `404 not_found`.

        Every test call sends a request to your URL and records one delivery,
        so test calls are limited to 10 a minute for a workspace, counted in
        fixed 60 second windows, on top of the request limits: over it the
        answer is `429 rate_limited` with `Retry-After`, whatever the id is.
        The route takes no body, no query parameter and no `Idempotency-Key`.
        Not yet enabled for every workspace: until yours is released a call
        answers `503 feature_disabled`. Requires `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/SubscriptionId"
      responses:
        "200":
          description: Whether the endpoint acknowledged the test event, and the public outcome.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookTestResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/WebhookTestLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /webhook-subscriptions/{subscriptionId}/rotate:
    post:
      operationId: webhooks_manage_rotate
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Rotate a subscription's signing secret.
      description: >-
        Makes a new signing secret and answers it in `secret` once. The
        previous key keeps signing beside the new one for 24 hours, so a
        delivery carries two signatures (one entry for each key id) until the
        old key retires, and a receiver that already holds only the new secret
        verifies every delivery. The answer lists both key ids in
        `signingKeyIds`.

        `Idempotency-Key` is required: a missing or malformed key is `400
        invalid_request`. The same key for the same subscription answers `200`
        with the subscription and WITHOUT the secret and makes no further key;
        the same key for another subscription is `422 idempotency_key_reused`.
        A rotation that raced another change to the subscription is `409
        version_conflict` and writes nothing: read the subscription and try
        again. Another workspace's id, a deleted subscription and an id that
        was never issued answer the same `404 not_found`. The route takes no
        body and no query parameter. Not yet enabled for every workspace: until
        yours is released a call answers `503 feature_disabled`. Requires
        `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/SubscriptionId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The subscription and its new signing secret (absent on a replay of the same key).
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookSecret"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/WebhookVersionConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /webhook-subscriptions/{subscriptionId}/deliveries:
    get:
      operationId: webhooks_manage_list_deliveries
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: List the deliveries of a webhook subscription.
      description: >-
        The delivery log of one subscription, newest first, 25 a page by
        default and at most 200 with `limit`. Each row is a `WebhookDelivery`:
        its `dlv_` id (the `X-CoSignal-Delivery-Id` your endpoint received),
        the ids of its signal and source event, `type`, `status`, `attempts`,
        its times, the `lastStatusCode` and public `outcome` of the last
        attempt and `replayCount`, and never a body, a header, a URL, an
        internal reason or a discard reason. `status` narrows the list to one
        status (`dead_letter` is the dead letter view); any other value is `400
        invalid_request`. A cursor is bound to this subscription and this
        filter: a cursor that is malformed, was issued for another list or
        names a delivery this subscription does not hold is `400
        invalid_request`. Only `limit`, `cursor` and `status` are accepted: an
        unknown or repeated query parameter is `400 invalid_request`. Every
        delivery is listed for 30 days after it ends. Another workspace's id, a
        deleted subscription, an id that was never issued, a malformed id and
        an id of another type all answer the same `404 not_found`. Not yet
        enabled for every workspace: until yours is released a call answers
        `503 feature_disabled`. Requires `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/SubscriptionId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: status
          in: query
          required: false
          description: Only the deliveries in this status.
          schema:
            type: string
            enum:
              - pending
              - delivered
              - retry_scheduled
              - dead_letter
              - requires_correction
              - endpoint_disabled
              - dropped
              - discarded
      responses:
        "200":
          description: One page of the subscription's deliveries.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryList"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /deliveries/{deliveryId}:
    get:
      operationId: webhooks_manage_read_delivery
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Read one webhook delivery.
      description: >-
        One delivery as a `WebhookDelivery`, the same fifteen fields the log
        lists and nothing else. `lastAttemptAt`, `lastStatusCode` and `outcome`
        describe the last attempt MADE: after a replay they keep the previous
        run's values until the first attempt of the new run lands. Another
        workspace's id, an id that was never issued, a malformed id and an id of
        another type all answer the same `404 not_found`. The route accepts no
        query parameter. Not yet enabled for every workspace: until yours is
        released a call answers `503 feature_disabled`. Requires
        `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/DeliveryId"
      responses:
        "200":
          description: The delivery.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /deliveries/{deliveryId}/replay:
    post:
      operationId: webhooks_manage_replay_delivery
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Replay a webhook delivery that ended.
      description: >-
        Sends a delivery that ended again and answers `202` with the delivery,
        which reads `pending` with a `replayCount` one higher. A replay is an
        enqueued attempt, never a send: the attempt is made right after by the
        same path as the first, so the same `X-CoSignal-Delivery-Id` and the
        same body (the original event `time`) reach your endpoint with a fresh
        signature. It gets a fresh budget of 12 attempts and 24 hours. Until
        the first attempt of the new run lands, `lastAttemptAt`,
        `lastStatusCode` and `outcome` keep the previous run's values.

        A replay is checked again against your current access exactly as a
        first delivery is, so it never sends data that is no longer yours to
        receive. It is refused with the one `409 delivery_not_replayable`,
        which never says why, when that data was withdrawn, when the
        subscription is not `active` or no longer lists the type, when the
        credential that made the subscription no longer qualifies, or when the
        delivery cannot be replayed (a `discarded` delivery, a test event, or
        one that is still being retried); nothing is written. A caller without
        `ecosystem:webhooks:manage` is refused `403 insufficient_scope`.
        Another workspace's id, an id that was never issued, a malformed id and
        an id of another type all answer the same `404 not_found`. The route
        takes no body, no query parameter and no `Idempotency-Key`: a second
        replay of the same delivery is refused with the `409` once the first is
        pending. Not yet enabled for every workspace: until yours is released a
        call answers `503 feature_disabled`. Requires
        `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/DeliveryId"
      responses:
        "202":
          description: The replay was accepted; the delivery is `pending` again.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DeliveryNotReplayable"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /deliveries/{deliveryId}/discard:
    post:
      operationId: webhooks_manage_discard_delivery
      x-cosignal-operation: webhooks_manage
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: released_per_workspace
      tags: [Webhooks]
      summary: Discard a dead lettered webhook delivery.
      description: >-
        Moves a `dead_letter` delivery to `discarded` with your own note. The
        body is `{ "reason": "..." }` of 1 to 200 characters (counted as
        characters, so 200 emoji are accepted and 201 are refused; surrounding
        white space does not count); a reason outside that length, a body with
        another key, anything that is not JSON and any query parameter are `400
        invalid_request`. The reason is stored with the delivery for you and is
        never shown back. Only a dead letter can be discarded: any other status
        answers the same `409 delivery_not_replayable` as a refused replay. A
        discarded delivery cannot be replayed and is listed for 30 days. A
        caller without `ecosystem:webhooks:manage` is refused `403
        insufficient_scope`. Another workspace's id, an id that was never
        issued, a malformed id and an id of another type all answer the same
        `404 not_found`. The route takes no `Idempotency-Key`: discarding is
        idempotent in effect, and a second discard is the `409`. Not yet
        enabled for every workspace: until yours is released a call answers
        `503 feature_disabled`. Requires `ecosystem:webhooks:manage`.
      security:
        - ecosystemClientCredentials: [ecosystem:webhooks:manage]
      parameters:
        - $ref: "#/components/parameters/DeliveryId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookDeliveryDiscard"
      responses:
        "200":
          description: The delivery, now `discarded`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/DeliveryNotReplayable"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /greenfield:
    get:
      operationId: greenfield_read
      x-cosignal-operation: greenfield_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Greenfield]
      summary: Find new accounts through one partner.
      description: |-
        The stored greenfield candidates for ONE partner, ranked by the
        computation the Intelligence page in the app uses; this route never
        re ranks them and never lets a model change an order, an inclusion
        or a score. Requires both `ecosystem:intelligence:read` and
        `ecosystem:partners:read`. `partner` is required: what a partner
        shares with you is decided partner by partner.

        `direction` is `partner_to_us` (the default): accounts your partner
        has that you do not; or `us_to_partner`: your accounts the partner
        does not have.

        `mode` is `top_prospects` (the default), which orders by the
        computed `rank`, highest first, and maps to the documented Top
        Prospects ordering; or `recent_partner_customer`, which keeps only
        candidates whose stored account type is customer, orders them by the
        `Recent activity` factor, highest first, and then by `rank`, and
        maps to the documented Recent Partner Customer ordering. In either
        mode ties order by the candidate's internal id, identical on every
        repeat. A third mode is not documented and is refused like any
        unknown value. A candidate stored before its account type was
        recorded is not a customer.

        Each row carries its `rank`, the `factors` behind it (each with the
        contribution it made and the stored sentence that explains it), its
        ICP fit tier and score, and its `commercialPotential`. `meta.factors`
        defines each factor and the most it can contribute, the same on every
        response. The row's `id` is a `gfc_` public id minted to your
        workspace; no partner internal account id is ever returned.

        `where` filters the candidates on firmographic fields, with the same
        predicate grammar as the overlap searches (one to five predicates).
        With `direction=partner_to_us` a field can be used only when the
        partner shares it with you and it is marked filterable, such as
        `account.industry` or `account.employee_count`; with
        `direction=us_to_partner` the fields are your own `account.industry`
        and `account.employee_count`. Any other field (withheld, unknown, not
        filterable such as `account.annual_revenue`, a name or domain, a lead
        field, or chosen by another workspace) is refused exactly like an
        unknown one, with the same `400 invalid_request`. A value a partner
        does not share never satisfies a predicate.

        When a partner shares nothing with you, when its channel settings
        refuse the API, when it shares counts only, when it has no stored
        candidates and when nothing matches, the answer is the same empty
        page with `meta.state` of `no_candidates`, so a result never hints at
        what a partner keeps private. A malformed, unknown or foreign
        `partner` is the same `400 invalid_request`. Every response carries
        the limitation `partner_channel_settings_apply`, whether or not
        anything was withheld.

        The in app Intelligence view is admin only, so a member's own token
        is answered the empty page and an admin created key or a workspace
        admin reads the candidates.

        With `explain=true`, each of the first five rows of the page carries
        an `explanation`: a short phrasing of that row's own factors. Every
        explanation is built from the computed result only, is checked
        against it, and falls back to a plain template whenever a check
        fails or model phrasing is not switched on (`source` says which).
        Rows beyond the first five carry `explanation: null` and the page
        lists `explanations_limited_to_first_rows`. Without `explain` no row
        carries the key. A cursor is bound to the query it was issued for.
      security:
        - ecosystemClientCredentials: [ecosystem:intelligence:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: partner
          in: query
          required: true
          description: >-
            A `ptsh_` partnership id from `GET /partners`: the one partner
            whose candidates are read.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: direction
          in: query
          required: false
          description: >-
            `partner_to_us` (the default): the partner's accounts you do not
            have. `us_to_partner`: your accounts the partner does not have.
          schema:
            type: string
            enum: [partner_to_us, us_to_partner]
        - name: mode
          in: query
          required: false
          description: >-
            `top_prospects` (the default): the computed rank, highest first.
            `recent_partner_customer`: customers only, most recent activity
            first, then rank.
          schema:
            type: string
            enum: [top_prospects, recent_partner_customer]
        - name: where
          in: query
          required: false
          description: >-
            A JSON encoded array of one to five predicates, each exactly
            `{ "field": string, "operator": string, "value": string or number }`,
            all of which a candidate must satisfy. `operator` is one of
            `equals`, `contains`, `greater_than`, `less_than`, `before` and
            `after`; text comparisons ignore case. A field must be disclosed
            to you and marked filterable, as described above; any other field
            is refused with `400 invalid_request`.
          schema:
            type: string
            minLength: 1
            maxLength: 24000
        - name: explain
          in: query
          required: false
          description: >-
            `true` attaches an explanation to the first five rows of the page.
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: One page of candidates, or the empty page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GreenfieldResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /recommendations:
    get:
      operationId: recommendations_read_list
      x-cosignal-operation: recommendations_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Recommendations]
      summary: Recommend which partner to involve on which opportunity.
      description: |-
        The Deal Navigator ranking: which partner to involve on which open
        opportunity of which account, computed by the same rules and read
        through the same disclosure the Intelligence page in the app uses.
        This route never re ranks the result and never lets a model change an
        order, an inclusion or a score. Requires both
        `ecosystem:intelligence:read` and `ecosystem:partners:read`.

        Rows come highest `score` first, ties by the nearer close date, then
        the larger amount, then a fixed internal order that is identical on
        every repeat. One row is one opportunity through one partner. Each row
        carries its `score` (0 to 100, the sum of seven fixed factor
        contributions, rounded) and `band` (`High` 75 and above, `Medium` 50
        to 74, `Low` below 50), the `factors` behind it with what each
        contributed and the evidence sentence for it, `evidenceCount` (the
        number of stored evidence records behind the row), `evidenceUpdatedAt`
        and the `window` the evidence covers: `start` is the day it was
        generated (inclusive) and `end` the day it expires (exclusive), null
        when it has no stored expiry. `meta.definitions` states what each of
        these means, the same on every response. The row's `id` is a `rec_`
        public id minted to your workspace; the account, opportunity and
        partner are named by their `acct_`, `opp_` and `ptsh_` ids, and no
        internal id is ever returned.

        `account`, `opportunity` and `partner` narrow the rows to those ids of
        your own workspace and never reorder them. An id that was never issued
        to you, belongs to another workspace or is of another kind is the same
        `400 invalid_request`. A filter nothing matches is the ordinary empty
        page.

        A partner that shares nothing relevant with you, whose channel
        settings refuse the API, that shares counts only or that withholds the
        opportunity or contact details contributes no row, and every one of
        those causes answers the same empty page, so a result never hints at
        what a partner keeps private. Every response carries the limitation
        `partner_channel_settings_apply`, whether or not anything was
        withheld.

        An admin created key and a workspace admin read the whole workspace. A
        member's own token reads only the rows of
        the accounts that member owns or claimed, the same view the app shows
        them; asking for an account they do not own answers the same empty
        page as asking for one they own with nothing to recommend.

        With `explain=true`, each of the first five rows of the page carries
        an `explanation`: a short phrasing of that row's own factors. Every
        explanation is built from the computed result only, is checked
        against it, and falls back to a plain template whenever a check fails
        or model phrasing is not switched on (`source` says which). Rows
        beyond the first five carry `explanation: null` and the page lists
        `explanations_limited_to_first_rows`. Without `explain` no row carries
        the key. A cursor is bound to the query it was issued for.
      security:
        - ecosystemClientCredentials: [ecosystem:intelligence:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: account
          in: query
          required: false
          description: >-
            An `acct_` id of one of your own accounts: only recommendations
            for that account.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: opportunity
          in: query
          required: false
          description: >-
            An `opp_` id of one of your own opportunities: only
            recommendations for that opportunity.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: partner
          in: query
          required: false
          description: >-
            A `ptsh_` partnership id from `GET /partners`: only
            recommendations through that partner.
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - name: explain
          in: query
          required: false
          description: >-
            `true` attaches an explanation to the first five rows of the page.
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: One page of recommendations, or the empty page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecommendationsResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /partners/{partnerId}/context:
    get:
      operationId: recommendations_read_partner_context
      x-cosignal-operation: recommendations_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Recommendations]
      summary: Read one partner's score and what it rests on.
      description: |-
        One partner's score from your own stored partner score snapshot, read
        through the same disclosure the Intelligence page in the app uses and
        computed by the same rules; this route never computes a score of its
        own and never lets a model change a figure. Requires both
        `ecosystem:intelligence:read` and `ecosystem:partners:read`.
        `partnerId` is the partnership's `ptsh_` id: anything else is
        `400 invalid_request` on shape alone.

        `score` and `band` are the partner's displayed score and its band
        (`Leading` 80 and above, `Established` 60 to 79, `Developing` below
        60, `Insufficient evidence` when nothing was measured, in which case
        `score` is null, never zero). `dimensions` lists the five score
        dimensions in a fixed order, each with its `value` (0 to 100),
        `weight`, `contribution` (value times weight) and `shareOfScore` (its
        contribution divided by the total of the measured contributions); a
        dimension that was not measured has null `value`, `contribution` and
        `shareOfScore` and `measured: false`. `definitions` says what each
        dimension measures and `denominators` says what every ratio is
        divided by: `shareOfScore` is the total of the measured contributions
        and `evidenceCoverage` the number of dimensions. A ratio whose
        denominator is zero or absent is `null`, never zero and never NaN.
        `period`, `comparisonPeriod`, `freshness` and `weightVersion` state
        which computation this is, and `window` states the span the metrics
        cover as an inclusive `start` and an exclusive `end` in ISO dates
        (both null when the stored period is not a span the API can state).
        The stored metric ids behind a dimension are never returned.

        A partner that was never issued to you, belongs to another workspace,
        whose channel settings refuse the API, or that has no stored score all
        answer the same `404 not_found`. The partner score view is an admin
        view in the app, so a member's own token
        is answered `200` with `data: null` and `meta.emptyReason` of
        `member_admin_view_only`, whatever partner is named; an admin created
        key or a workspace admin reads the context. Every response carries the
        limitation `partner_channel_settings_apply`.

        With `explain=true`, `data.explanation` carries a short phrasing of
        the figures above, built from the computed result only, checked
        against it, and falling back to a plain template whenever a check
        fails or model phrasing is not switched on (`source` says which).
        Without `explain` the key is absent.
      security:
        - ecosystemClientCredentials: [ecosystem:intelligence:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/PartnerId"
        - name: explain
          in: query
          required: false
          description: >-
            `true` attaches one explanation to the context.
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: The partner's score context, or the member's empty answer.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContextResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /attribution:
    get:
      operationId: attribution_read
      x-cosignal-operation: attribution_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Attribution]
      summary: Read what each partner is credited with in your stored rollup.
      description: |-
        One row per partner from your stored intelligence rollup, the same
        figures the Intelligence page in the app shows, read through the same
        disclosure; this route computes no attribution of its own. Requires
        `ecosystem:attribution:read`.

        Each row carries `opportunityCount` (the opportunities the partner is
        credited on), `primaryOpportunityCount` (how many of those it is the
        primary credit for), `primaryShare` (the second divided by the first,
        or `null` when `opportunityCount` is zero, never zero and never NaN),
        `attributedValue` (the summed value of those opportunities; an
        opportunity credited to several partners counts in full for each),
        the rollup `period` (`6m` is six months) and the `window` it states as
        an inclusive `start` and an exclusive `end` in ISO dates, ending the
        day after the rollup was produced (`updatedAt`). `denominators` says
        what each ratio is divided by, and `meta.definitions` states what
        every field means and where the figures come from, the same on every
        response. The row's partner is named by its `ptsh_` id; no internal id
        is ever returned. Rows come highest `attributedValue` first, ties by
        a fixed internal order that is identical on every repeat.

        Co-Signal calculates that rollup from your own closed and marked
        co-sell requests and your CRM opportunities, a few minutes after a
        change and again every night.

        The figures come from the stored intelligence rollup only, and every
        response carries the limitation `attribution_from_stored_rollup`: no
        partner is listed until a rollup has been produced for your workspace,
        so an empty page can mean none exists yet. The workspace wide totals
        of that rollup are never returned, because they sum every partner and
        do not follow each partner's channel settings.

        A partner that shares nothing with you, or whose channel settings
        refuse the API, contributes no row, and every one of those causes
        answers the same page as a partner with no stored row, so a result
        never hints at what a partner keeps private. Every response carries
        `partner_channel_settings_apply`, whether or not anything was
        withheld. `partner` (a `ptsh_` id of your own) narrows the page to one
        partner; an id that was never issued to you, belongs to another
        workspace or is of another kind is `400 invalid_request`.

        A partnership that has ended contributes no row, and the page is the
        same as if it had never been credited; the credit it earned before it
        ended still counts in your workspace's totals in the app.

        Attribution is an admin view in the app. An admin created key and a
        workspace admin read the rows; a member's own token
        is answered `200` with no rows and `meta.emptyReason` of
        `member_admin_view_only`, whatever partner is named. A cursor is
        bound to the query it was issued for.
      security:
        - ecosystemClientCredentials: [ecosystem:attribution:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: partner
          in: query
          required: false
          description: >-
            A `ptsh_` partnership id from `GET /partners`: only that partner's
            row.
          schema:
            type: string
            minLength: 1
            maxLength: 100
      responses:
        "200":
          description: One page of partner attribution rows, the empty page, or the member's empty answer.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AttributionResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /activity:
    get:
      operationId: activity_read_workspace
      x-cosignal-operation: activity_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Activity]
      summary: Read your workspace's co-sell activity, newest first.
      description: |-
        Your own workspace's co-sell request activity: a `request_sent` event
        at the time your workspace sent a request and a `request_answered`
        event at the time it accepted or declined one it received, newest
        first with the true stored timestamp (`occurredAt`). Two events with
        the same timestamp order by event id, descending, identically on every
        request. Each event names the partner by its `ptsh_` id and the request
        by its `req_` id with its type and current status; it never says who in
        either workspace acted. Requires `ecosystem:activity:read`.

        `period` is `30d`, `90d` (the default) or `quarter`. `meta.window`
        states the period and the window it covers: `start` is inclusive and
        `end` is the moment of the read; `30d` and `90d` are rolling and
        `quarter` is the calendar quarter so far, in UTC. `meta.summary` is the
        same summary the Performance tab in the app shows for that window:
        `requestsSent`, `requestsAnswered`, `accountsInfluenced` and `byPartner`
        counts; it never carries a per member breakdown, because a member
        directory is an organization wide power. An empty period answers empty
        `data`, a zero summary and the stated window. `meta.definitions`
        states what each figure means. Pages are positional (`limit`,
        `cursor`) and a cursor is bound to the query it was issued for.

        Activity reads your workspace's own requests, so a partner's channel
        settings do not change it. An admin created key and a workspace admin
        read the whole workspace. A member's own token
        reads only the requests that member sent and the received
        requests on accounts that member owns, with a summary over just those.
      security:
        - ecosystemClientCredentials: [ecosystem:activity:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: period
          in: query
          required: false
          description: >-
            The window: the last 30 days, the last 90 days (the default) or the
            calendar quarter so far.
          schema:
            type: string
            enum: ["30d", "90d", quarter]
      responses:
        "200":
          description: One page of activity events, or the empty page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActivityResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /accounts/{accountId}/timeline:
    get:
      operationId: activity_read_account_timeline
      x-cosignal-operation: activity_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Activity]
      summary: Read the timeline of one of your accounts, newest first.
      description: |-
        The activity on one of your own accounts, by its `acct_` public id,
        newest first: internal notes your team, your keys and your approved
        apps wrote, and the requests, drafts, CRM syncs, commands and
        automated sends Co-Signal recorded on the account. Each entry has its
        own `tle_` id, a `kind`, who or what made it (`actor`, a kind and a
        name, never an id) and the `act_` id of its action. A note carries its
        `body`; any other entry carries a fixed `template` key and a few
        `params` (a provider name, a mapping version, counts) and never a
        message or a partner's value. Requires `ecosystem:activity:read`; this
        is a second path under the same operation as `GET /api/v1/activity`.

        Entries order by time, newest first, and two entries with the same
        time order by entry id, descending, identically on every request.
        Pages are positional: at most 50 entries, `limit` (1 to 50) and an
        opaque `cursor`. An account with no entries answers an empty `data`,
        `hasMore` false and a null `nextCursor`.

        The timeline holds your workspace's own entries and no partner shared
        value, so a partner's channel settings do not change it. An admin
        created key and a workspace admin read every account of the workspace;
        a member's own token reads only the accounts that member owns or
        claimed. An account you may not see (another workspace's, one outside
        that rule, an id that was never issued, a malformed id) answers ONE
        `404 not_found`: an absent id and an unauthorized id are deliberately
        indistinguishable.
      security:
        - ecosystemClientCredentials: [ecosystem:activity:read]
      parameters:
        - $ref: "#/components/parameters/AccountId"
        - $ref: "#/components/parameters/TimelineLimit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of timeline entries, or the empty page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /timeline/entries:
    post:
      operationId: timeline_write
      x-cosignal-operation: timeline_write
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Activity]
      summary: Add an internal note to an account's timeline.
      description: |-
        Adds an internal note to the timeline of one of your accounts: the
        account by its `acct_` id and a `body` of 1 to 1,000 characters after
        trimming (anything else is `400 invalid_request`). Requires
        `ecosystem:timeline:write`.

        A note is internal to your workspace. It is never sent to a partner
        and never triggers a message, a notification or a CRM write. It is
        attributed to your key (or to the app and person using it), carries the
        `act_` id of the action that recorded it, and cannot be changed or
        deleted through the API.

        Send an `Idempotency-Key`: a retry with the same key and the same body
        answers the first `201` and writes nothing, and the same key with
        another body is `422 idempotency_key_reused`. A note's key is
        remembered for 400 days. An account you may not see answers the same
        ONE `404 not_found` as the timeline read, whatever the reason.
      security:
        - ecosystemClientCredentials: [ecosystem:timeline:write]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimelineEntryCreateRequest"
      responses:
        "201":
          description: The new entry and the action that recorded it.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineEntryCreateResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /drafts:
    post:
      operationId: drafts_write
      x-cosignal-operation: drafts_write
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Actions]
      summary: Prepare a co-sell request for a named person to approve.
      description: |-
        Prepares a co-sell request on one of your own accounts for one of your
        partnerships: the account by its `acct_` id, the partnership by its
        `ptsh_` id (from `GET /partners`), an optional `cont_` partner contact,
        the `type` (`intro` or `context`) and a `message` of 1 to 2,000
        characters after trimming. Requires `ecosystem:drafts:write`.

        A draft is a pending item. Nothing reaches the partner from this call:
        a member of your workspace approves the draft in Co-Signal, at the
        `approvalUrl` the answer carries, within seven days (`expiresAt`), and
        only then is the request sent, with that member as its sender. No
        automation policy ever sends a draft, whoever asks. Follow it with
        `GET /actions/{actionId}`.

        The account must be visible for that partnership on the automation
        channel: your own account that the partnership overlaps, while the
        partner's automation channel setting allows it. An account you may not
        see, a partnership that is not yours, a partner contact that is not
        one of that partner's and an id that was never issued or is malformed
        all answer the same ONE `404 not_found`.

        Send an `Idempotency-Key`: a retry with the same key and the same body
        answers the first `202` and creates nothing more, and the same key with
        another body is `422 idempotency_key_reused`. The key is remembered for
        400 days.
      security:
        - ecosystemClientCredentials: [ecosystem:drafts:write]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DraftCreateRequest"
      responses:
        "202":
          description: The draft waits for a named person to approve it.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingActionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /requests:
    post:
      operationId: requests_act
      x-cosignal-operation: requests_act
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Actions]
      summary: Create a co-sell request for a named person to approve.
      description: |-
        Creates a co-sell request on one of your own accounts for one of your
        partnerships, with the same body, visibility rule and refusals as
        `POST /drafts`. Requires `ecosystem:requests:act`.

        An integration key's request is a pending item. Nothing reaches the
        partner from this call: a member of your workspace approves it in
        Co-Signal, at the `approvalUrl` the answer carries, within seven days
        (`expiresAt`), and only then is it sent, with that member as its
        sender (`202`). The one exception is a person's own delegated token
        for a member whom an admin's active automation policy lists and whose
        exact request that policy permits: that request is sent at once under
        the policy and answers `201` with the `request` it sent. A key is
        never eligible for a policy, so a key always gets `202`. Follow either
        answer with `GET /actions/{actionId}`.

        Send an `Idempotency-Key`: a retry with the same key and the same body
        answers the first response (`202` or `201`) and sends nothing more,
        and the same key with another body is `422 idempotency_key_reused`.
        The key is remembered for 400 days.
      security:
        - ecosystemClientCredentials: [ecosystem:requests:act]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RequestCreateRequest"
      responses:
        "201":
          description: An automation policy permitted the exact request and it was sent.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestCreatedResponse"
        "202":
          description: The request waits for a named person to approve it.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingActionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /requests/{requestId}/accept:
    post:
      operationId: requests_respond_accept
      x-cosignal-operation: requests_respond
      x-cosignal-principal-restriction: delegated_person_only
      x-cosignal-availability: checkpoint_1
      tags: [Actions]
      summary: Accept a co-sell request a partner sent you, as a person.
      description: |-
        Accepts a co-sell request that a partner sent to your workspace, by its
        `req_` id, with an optional `replyNote`. Requires
        `ecosystem:requests:act` AND a person's own delegated token: accepting,
        declining and closing are person actions, so an integration key,
        whatever scopes it holds, is refused `403 insufficient_scope` exactly as
        a key without the scope is. A person's own delegated token, which a signed
        in person gets by consenting in Co-Signal for a workspace in the API
        rollout, is the only credential that can call these three routes.

        The answer is attributed to the person whose token made it. Only a
        current member of the receiving workspace sees the request at all:
        another workspace, the sender's own side, an id that was never issued
        and a malformed id answer the same ONE `404 not_found`. A request that
        has already been decided is `409 version_conflict` and nothing is
        written.

        Send an `Idempotency-Key`: a retry with the same key and the same
        answer returns the first `200` and writes nothing more, even after the
        request has moved on, and the same key with another answer is `422
        idempotency_key_reused`.
      security:
        - ecosystemClientCredentials: [ecosystem:requests:act]
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RequestRespondRequest"
      responses:
        "200":
          description: The request was accepted.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestAnsweredResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/RequestStateConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /requests/{requestId}/decline:
    post:
      operationId: requests_respond_decline
      x-cosignal-operation: requests_respond
      x-cosignal-principal-restriction: delegated_person_only
      x-cosignal-availability: checkpoint_1
      tags: [Actions]
      summary: Decline a co-sell request a partner sent you, as a person.
      description: |-
        Declines a co-sell request that a partner sent to your workspace, by its
        `req_` id, with an optional `replyNote`. It has the same actor rule,
        visibility, idempotency and refusals as
        `POST /requests/{requestId}/accept`, and requires a person's own
        delegated token: an integration key is refused `403 insufficient_scope`
        exactly as a key without `ecosystem:requests:act` is.
      security:
        - ecosystemClientCredentials: [ecosystem:requests:act]
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RequestRespondRequest"
      responses:
        "200":
          description: The request was declined.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestAnsweredResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/RequestStateConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /requests/{requestId}/close:
    post:
      operationId: requests_respond_close
      x-cosignal-operation: requests_respond
      x-cosignal-principal-restriction: delegated_person_only
      x-cosignal-availability: checkpoint_1
      tags: [Actions]
      summary: Close an accepted co-sell request you sent, as a person.
      description: |-
        Closes an accepted co-sell request that your workspace sent, by its
        `req_` id, with an `attribution` (`sourced`, `influenced` or `none`:
        how the partner affected the deal) and an optional `closeNote` of up to
        280 characters. It requires `ecosystem:requests:act` and a person's own
        delegated token: an integration key is refused `403 insufficient_scope`
        exactly as a key without the scope is.

        Only the person who sent the request, or a current admin of your
        workspace, sees it for this call; anyone else (the receiving side,
        another member, another workspace, an id that was never issued or is
        malformed) gets the same ONE `404 not_found`. A request that is not
        accepted yet, or is already closed, is `409 version_conflict`.

        Send an `Idempotency-Key`: a retry with the same key and the same
        answer returns the first `200`, and the same key with another answer
        is `422 idempotency_key_reused`.
      security:
        - ecosystemClientCredentials: [ecosystem:requests:act]
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RequestCloseRequest"
      responses:
        "200":
          description: The request was closed.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestAnsweredResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/RequestStateConflict"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /actions/{actionId}:
    get:
      operationId: actions_read
      x-cosignal-operation: actions_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Actions]
      summary: Read the status of one action.
      description: |-
        The status of one action a draft, a request, a timeline note or a CRM
        command started, by its `act_` id: its `kind`, its `status`, exact `counts`, closed
        failure `code`s and the resource it produced as `result`, a public id
        in your workspace. A pending item reads `pending_approval` with the
        `approvalUrl` to approve it at and the end of its window
        (`expiresAt`); once a named person approves it, it reads `succeeded`
        and `result` is the `req_` id of the request that was sent. A message
        or a note body is never returned. Requires any one of
        `ecosystem:drafts:write`, `ecosystem:requests:act`,
        `ecosystem:timeline:write` or `ecosystem:crm:write`.

        Only the action's owner (the same key, or the same app connection) or a
        current admin acting through a delegated token reads it. Another key of
        your workspace, another workspace, an id that was never issued, a
        malformed id and an id of another type each answer the same ONE `404
        not_found`: an absent id and an unauthorized id are deliberately
        indistinguishable. A pending item whose seven days have passed reads
        `expired`.
      security:
        - ecosystemClientCredentials: [ecosystem:drafts:write]
        - ecosystemClientCredentials: [ecosystem:requests:act]
        - ecosystemClientCredentials: [ecosystem:timeline:write]
        - ecosystemClientCredentials: [ecosystem:crm:write]
      parameters:
        - $ref: "#/components/parameters/ActionId"
      responses:
        "200":
          description: The status of the action.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /crm/accounts/{accountId}/push-fields:
    post:
      operationId: crm_push_account_fields
      x-cosignal-operation: crm_push_account_fields
      x-cosignal-principal-restriction: approved_app_crm_command
      x-cosignal-availability: released_per_workspace
      tags: [Actions]
      summary: Push one account's overlap fields into your CRM, after a preview.
      description: |-
        Co-Signal extension: not a Crossbeam equivalent.

        Writes the overlap fields of one of your accounts, by its `acct_` id,
        into your workspace's connected CRM (a Salesforce Account or a HubSpot
        company) through its Co-Signal destination, exactly as the destination
        mapping an admin approved describes them. Requires
        `ecosystem:crm:write`. Two steps on this one route.

        **Preview.** Send `{ "provider": "salesforce" | "hubspot", "mode":
        "preview" }`. The answer is `200` with a `payloadHash`, the `provider`,
        the destination `destinationType`, the `mappingVersion` and one record
        for each row the push would write, each naming its target by a public
        id of your workspace and its `fields` by CRM API name. It never carries
        a value: an app reads partner data only through the channel its
        transport is allowed, and the values are written by Co-Signal under CRM
        sync. A preview writes nothing.

        **Execute.** Send `{ "provider", "payloadHash" }` with the hash the
        preview answered and an `Idempotency-Key`. Co-Signal recomputes the
        preview and writes only if it still has that hash: `202` with the
        action to follow with `GET /actions/{actionId}`. If the preview no
        longer holds, for any cause (the payload changed, the mapping or a
        partner's sharing changed, the record no longer qualifies), the answer
        is the ONE `409 preview_stale` and nothing is written; preview again. A
        retry with the same key and hash answers the first `202` and writes
        nothing more, and the same key with another hash is `422
        idempotency_key_reused`. A missing key on execute is `400`.

        **Off by default, and never a key's power.** The command answers `503
        feature_disabled`, byte for byte as the API does, until Co-Signal
        releases CRM commands for your workspace. After that only a person who
        is a workspace admin now, holding `ecosystem:crm:write` through an app
        an admin switched THIS command on for in Settings > API, can run it.
        An integration key, whatever scopes it holds, a newly approved app, an
        app with only the other command, a member, an admin previewing as a
        member, a holder who was demoted, an app whose approval was revoked and
        a workspace whose destination is turned off are each refused `403
        insufficient_scope` exactly as a caller without the scope is. An
        account id that was never issued, one issued to another workspace and a
        malformed one answer the same ONE `404 not_found`.
      security:
        - ecosystemClientCredentials: [ecosystem:crm:write]
      parameters:
        - $ref: "#/components/parameters/AccountId"
        - $ref: "#/components/parameters/CommandIdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CrmCommandPreviewRequest"
                - $ref: "#/components/schemas/CrmCommandExecuteRequest"
      responses:
        "200":
          description: The preview of the push, with the hash an execute must send back. Carries no value.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCommandPreviewResponse"
        "202":
          description: The push was accepted; follow it with `GET /actions/{actionId}`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCommandAcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/PreviewStale"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /crm/overlaps/{overlapId}/sync:
    post:
      operationId: crm_sync_overlap_record
      x-cosignal-operation: crm_sync_overlap_record
      x-cosignal-principal-restriction: approved_app_crm_command
      x-cosignal-availability: released_per_workspace
      tags: [Actions]
      summary: Sync one overlap record into your CRM, after a preview.
      description: |-
        Co-Signal extension: not a Crossbeam equivalent.

        Writes exactly ONE overlap record (one of your accounts and one
        partner, named by its `aovl_` id) into your workspace's connected CRM
        through its Co-Signal destination. Requires `ecosystem:crm:write`. It is
        the same two steps as `POST /crm/accounts/{accountId}/push-fields`: a
        `{ "provider", "mode": "preview" }` call answers the `payloadHash`, the
        one target record and its CRM field API names, never a value; a
        `{ "provider", "payloadHash" }` call with an `Idempotency-Key` writes
        only that preview and answers `202` with the action to follow with
        `GET /actions/{actionId}`, or the ONE `409 preview_stale` when the
        preview no longer holds, for any cause. An `aovl_` id comes from the
        change feed's `account_overlaps` resource or from a push preview's
        overlap targets.

        An `aovl_` id is minted to your workspace only. One issued to another
        workspace, one never issued, a malformed one and an id of another type
        answer the same ONE `404 not_found`. The command is off by default and
        never a key's power, on the same terms as the push route: `503
        feature_disabled` until Co-Signal releases CRM commands for your
        workspace, and then only a current workspace admin's app that an admin
        switched THIS command on for; every other caller is refused `403
        insufficient_scope` exactly as a caller without the scope is.
      security:
        - ecosystemClientCredentials: [ecosystem:crm:write]
      parameters:
        - $ref: "#/components/parameters/OverlapId"
        - $ref: "#/components/parameters/CommandIdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CrmCommandPreviewRequest"
                - $ref: "#/components/schemas/CrmCommandExecuteRequest"
      responses:
        "200":
          description: The preview of the sync, with the hash an execute must send back. Carries no value.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCommandPreviewResponse"
        "202":
          description: The sync was accepted; follow it with `GET /actions/{actionId}`.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CrmCommandAcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/PreviewStale"
        "422":
          $ref: "#/components/responses/IdempotencyKeyReused"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /suggestions:
    get:
      operationId: suggestions_read
      x-cosignal-operation: suggestions_read
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: checkpoint_1
      tags: [Suggestions]
      summary: Read suggested partner companies from the Co-Signal directory.
      description: |-
        Companies that chose to join the Co-Signal directory (`origin`
        `opt_in`) or come from directory data Co-Signal is licensed to use
        (`origin` `licensed`), ranked for your workspace by named factors that
        compare each company with your own profile, your own active partners
        and your own connected CRM. Requires `ecosystem:discovery:read`.

        An organization wide power. An admin created key with the Suggestions
        capability reads it, and a signed in person's token is refused unless
        they are a current admin and an admin has approved the app for partner
        suggestions. Every other principal (a member, an admin previewing as a
        member, a demoted admin, and an admin whose app lacks the power) is
        refused with the same `403 insufficient_scope` as a key without the
        scope.

        Switched on in this release. Co-Signal has not loaded any licensed
        directory data yet, so every company listed today has `origin`
        `opt_in`. If the directory is switched off again, a request that
        passes the scope check answers
        `503 feature_disabled`, the same answer the API gives a workspace
        outside the rollout, before any query parameter is read, and the MCP
        tool `get_partner_suggestions` is not listed. An admin creating a key
        at Settings > API can choose the Suggestions capability.

        Each row carries a `dir_` public id minted to your workspace, the
        company `name`, `domain`, `category` and the `sellsWith` list it
        published, its `origin`, the `score` and the `factors` that make it
        (`meta.factors` says what each factor means and the most it can add),
        one `evidence` sentence, `inviteAvailable` and `invite`. `invite.href`
        opens the Invite a partner dialog on your Partnerships page; this API
        never sends an invitation, and `invite` is null for a licensed company
        whose source does not allow invitations. A `licensed` row carries
        `provenance`: for each field it shows, the `provider` and the
        `observedAt` time of the observation, and null for a field it does not
        show. The source's own id and licence reference are never returned,
        and neither are a company's customers, accounts, partners or internal
        ids.

        Every response carries `meta.coverage`: `shown` (the rows in `data`)
        and `totalListed`, every company counted for you by the same read
        (`optInListed` plus `licensedListed`). It never states the size of any
        wider network. A company that leaves the directory, and a licensed
        record or source that is withdrawn, is absent from the very next
        response, and its `dir_` id is never returned again. An empty `data`
        has `meta.state` of `no_candidates` and the same keys whatever the
        cause (an empty directory, every listed company already your partner,
        or none that scores), so the cause cannot be read from the page; only
        the counted listings in `meta.coverage` differ.

        `limit` is 1 to 25 (default 10) and a value outside that range is
        `400 invalid_request`, never a silent clamp. `explain=true` attaches an
        `explanation` to at most the first five rows, built only from that
        company's published facts and your own factor values, and it never
        changes the order or the score; every later row carries
        `explanation: null` and the response carries
        `explanations_limited_to_first_rows`. An unknown or repeated query
        parameter is `400 invalid_request`.
      security:
        - ecosystemClientCredentials: [ecosystem:discovery:read]
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of suggestions, 1 to 25, default 10. Out of range is `400 invalid_request`.
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 10
        - name: explain
          in: query
          required: false
          description: >-
            `true` attaches an explanation to at most the first five rows;
            `false` or absent attaches none.
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: The ranked suggestions with their coverage, or the empty `no_candidates` page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuggestionsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /knowledge/search:
    get:
      operationId: knowledge_search
      x-cosignal-operation: knowledge_search
      x-cosignal-principal-restriction: none
      x-cosignal-availability: shipped_not_in_checkpoint_1
      tags: [Knowledge]
      summary: Search the curated Co-Signal knowledge corpus.
      description: >-
        Deterministic lexical search over a static, reviewed corpus of first
        party Co-Signal documentation, never tenant data and never a live
        crawl. It requires `ecosystem:knowledge:read`, which any member can
        give through the consent screen, for a workspace in the API rollout;
        a key created at Settings > API cannot hold it. Retrieved text is
        untrusted source content, returned
        as a quoted citation with no action or command field. A query that
        looks like a URL is refused with `400 invalid_request` and is never
        resolved or fetched. A no result or stale document state is explicit
        in the body rather than an empty success.
      security:
        - ecosystemClientCredentials: [ecosystem:knowledge:read]
      parameters:
        - name: q
          in: query
          required: true
          description: Lexical search text. Refused when empty, over 200 characters, URL like or made only of stop words.
          schema:
            type: string
            minLength: 1
            maxLength: 200
        - name: limit
          in: query
          required: false
          description: Maximum number of document hits, 1 to 25, default 10. Out of range is `400 invalid_request`.
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 10
        - name: destinationId
          in: query
          required: false
          description: >-
            The explicit destination of this call, echoed in `meta`. An
            integration key may only name a destination an admin granted it;
            any other value is `400 invalid_request`.
          schema:
            type: string
            pattern: "^[A-Za-z0-9._:-]{1,128}$"
      responses:
        "200":
          description: Search completed. A no result or stale state is explicit in the body.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeSearchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /me:
    get:
      operationId: me
      x-cosignal-operation: me
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Identity]
      summary: Who is calling.
      description: >-
        The calling key's name, or the signed in person's `mem_` public id,
        display name and current role (read fresh on every call, so an admin
        previewing the workspace as a member reads `member`), the one
        workspace the credential belongs to, and the scopes it holds, sorted.
        The route accepts no query parameter at all. A key reaches it with the
        Identity capability.
      security:
        - ecosystemClientCredentials: [ecosystem:identity:read]
      responses:
        "200":
          description: The calling principal.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /workspaces:
    get:
      operationId: workspaces
      x-cosignal-operation: workspaces
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Identity]
      summary: The workspace the credential belongs to.
      description: >-
        Exactly one row: the one workspace the credential belongs to, with its
        `ws_` public id, name and domain, and `hasMore` false. Another
        workspace the signed in person also belongs to is never listed. Only
        `limit` and `cursor` are accepted: an unknown or repeated query
        parameter is `400 invalid_request`, and no cursor is ever issued.
      security:
        - ecosystemClientCredentials: [ecosystem:identity:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: The bound workspace.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /workspaces/current/members:
    get:
      operationId: workspace_members
      x-cosignal-operation: workspace_members
      x-cosignal-principal-restriction: admin_integration_only
      x-cosignal-availability: checkpoint_1
      tags: [Identity]
      summary: List the current members of your workspace.
      description: >-
        One `WorkspaceMember` per current member: the same `mem_` public id
        `GET /api/v1/me` returns for that person, their display name, work
        email, stored role and joined date, and nothing else. A removed member
        and a pending invitation are absent. Ordered by an internal stable key,
        not alphabetically. An organization wide power: an admin created key
        with the Member directory capability reads it; a signed in person's
        token is refused unless they are a current admin and an admin has
        approved the app for the member directory, and every refusal is the
        same `403 insufficient_scope`. Only `limit` and `cursor` are
        accepted: an unknown or repeated query parameter is
        `400 invalid_request`.
      security:
        - ecosystemClientCredentials: [ecosystem:directory:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: One page of your workspace's current members.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceMemberListResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /changes:
    get:
      operationId: changes_read
      x-cosignal-operation: changes_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: checkpoint_1
      tags: [Changes]
      summary: Take a snapshot of one resource, then follow its changes with a bookmark.
      description: |-
        One call binds ONE `resource`: `accounts`, `contacts`,
        `opportunities`, `leads`, `account_overlaps` or `lead_overlaps`.
        Start with a snapshot: call without `cursor` or `bookmark`, follow
        `page.nextCursor` to the last page, and keep the `bookmark` that page
        returns. Each snapshot item is an `upsert` carrying the resource's
        current row, the same row its own list route returns.

        Each later call with `bookmark` returns what changed since, oldest
        first, with a new bookmark to keep; `page.nextCursor` is then null,
        and `page.hasMore` true means call again at once. Changes appear
        after a short delay (a few seconds), so a change committed while a
        page is served is never lost behind your bookmark. A bookmark
        belongs to the credential, workspace, resource and query that created
        it, and one credential should page one bookmark at a time.

        A bookmark stays valid for 30 days, and change history is kept for 30
        days. An older bookmark answers `410 cursor_expired`; a bookmark
        whose view changed, because the credential's role or scopes changed,
        answers `410 resync_required`. Either way, take a new snapshot and
        replace your copy; the `resync` member names the call to make.

        The feed sends a record only when what your credential can see of it
        has changed since it last received it, so a client that discards its
        copy takes a new snapshot rather than rewinding a bookmark. When
        something you received is no longer available to you, the feed sends
        a `tombstone`: its id and change facts, never data and never a
        reason, the same whether the partner stopped sharing it, it left a
        shared population, it was deleted, or the partnership ended. Records
        you never received produce nothing. Overlap changes are sent per
        overlap: one item for each of your records and each partner it
        overlaps with, keyed by an `aovl_` or `lovl_` id.

        If a page's response never reaches you, call again with the same
        bookmark or cursor: what that page sent is sent again, and the
        bookmark you present next acknowledges it. A row whose stored value
        cannot be served within the response limits, such as a CSV name
        longer than 500 characters, is left out of a snapshot or change
        page, which then lists `rows_withheld_unservable` in
        `meta.limitations`; that row arrives once a later change makes it
        servable.

        Only synchronized copies are tracked: snapshots and change pages
        record what reached your credential, while ordinary list, detail and
        assistant reads record nothing and never produce tombstones. Data
        already delivered cannot be recalled.

        Each row of the own record resources carries its record's own CRM edit
        time in `provenance[].sourceUpdatedAt`, but this feed's
        `meta.sourceUpdatedAt` is always null, because a page spans many
        records, and it declares no `no_crm_source_timestamp` and carries no
        `meta.sourceFreshness`. A row's provider time can lag the CRM: a
        change to the CRM's edit time on its own sends no change item, so a
        row from this feed can show an older `sourceUpdatedAt` than the same
        record in an export or a list read. Read `meta.sourceFreshness` on a
        list read to see how fresh your data is.

        The route accepts any one of five read scopes (accounts, leads,
        contacts, opportunities or partners); each `resource` then requires
        its own list route's scopes, and a resource your credential cannot
        read is the same `403 insufficient_scope` as an operation it cannot
        use. `lead_overlaps` answers `503 feature_disabled` while lead
        overlaps are not released for your workspace. Only `resource`,
        `limit`, `cursor` and `bookmark` are accepted; `cursor` and
        `bookmark` together, an unknown or repeated parameter, and a cursor
        or bookmark that is malformed, issued to another credential or issued
        for another resource are all one `400 invalid_request`.
      security:
        - ecosystemClientCredentials: [ecosystem:accounts:read]
        - ecosystemClientCredentials: [ecosystem:contacts:read]
        - ecosystemClientCredentials: [ecosystem:leads:read]
        - ecosystemClientCredentials: [ecosystem:opportunities:read]
        - ecosystemClientCredentials: [ecosystem:partners:read]
      parameters:
        - name: resource
          in: query
          required: true
          description: The one resource this snapshot or bookmark binds.
          schema:
            type: string
            enum: [accounts, contacts, opportunities, leads, account_overlaps, lead_overlaps]
        - $ref: "#/components/parameters/Limit"
        - name: cursor
          in: query
          required: false
          description: The `page.nextCursor` of the previous snapshot page. Never sent with `bookmark`.
          schema:
            type: string
            maxLength: 1024
        - name: bookmark
          in: query
          required: false
          description: The `bookmark` of the last snapshot page or the previous change page. Never sent with `cursor`.
          schema:
            type: string
            maxLength: 1024
      responses:
        "200":
          description: One snapshot page or one change page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeFeedResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "410":
          description: >-
            `cursor_expired`: the bookmark or snapshot cursor is older than
            the 30 day window. `resync_required`: the credential's role or
            scopes changed since it was issued. Either way, take a new
            snapshot through `resync.href` and replace your copy.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeFeedResyncError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /signals/accounts:
    get:
      operationId: signals_accounts_read
      x-cosignal-operation: signals_accounts_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: released_per_workspace
      tags: [Signals]
      summary: Read the signals about your accounts, as a snapshot and then a bookmark.
      description: |-
        The account signal feed: the pull side of webhooks, for a system that
        cannot receive a webhook or that missed some. It carries the overlap
        signals (`overlap.created`, `overlap.updated` and `overlap.removed`)
        and the partner deal signals (`partner_deal_opened`,
        `partner_deal_closed_won` and `partner_greenfield_closed_won`) about
        your accounts. A partner deal signal is about a deal you could see in
        Co-Signal, and a partner greenfield signal follows the partner's
        record level sharing with you, never counts only.

        Start with a snapshot: call without `cursor` or `bookmark`, follow
        `page.nextCursor` to the last page, and keep the `bookmark` that page
        returns. A snapshot holds the signals committed before a cut-off a few
        seconds in the past, and the bookmark starts the change pages exactly
        at that cut-off, so no signal falls between the two. Every snapshot
        item has `historical: true`, because a snapshot is history.

        Each later call with `bookmark` returns the signals committed since,
        oldest first, with a new bookmark to keep; `page.nextCursor` is then
        null and `page.hasMore` true means call again at once. On a change
        page `historical` is `false` for a live change and `true` only for a
        signal that describes how things stood before signals were switched
        on for you. A page can hold fewer items than `limit`, or none, while
        `hasMore` is true, because every item is checked against what you can
        see now: keep calling until `hasMore` is false.

        A bookmark stays valid for 30 days, and signals are kept for 31 days
        and never served past their expiry. An older bookmark answers `410
        cursor_expired`. A bookmark whose view changed because the
        credential's role or scopes changed, a snapshot whose position signal
        expired between two pages, and a bookmark issued before an update to
        the change feed's contract answer `410 resync_required`; signal
        bookmarks share the change feed's contract version. Either way, take a
        new snapshot through `resync.href` and replace your copy.

        `overlap.removed` is a tombstone that never gives a reason and
        carries only the overlap id and `kind`. The `data` of an item equals the `data` of the
        webhook for the same signal. A workspace member sees only the signals
        about the records they own or have claimed, and an `overlap.removed`
        only while the record is still one of them, so member clients should
        also follow the accounts family of `GET /changes`, whose tombstone
        tells them to drop the account and its overlaps. A person whose
        membership has ended is refused `403 insufficient_scope`.

        `types` is a comma separated list of event type names this feed
        carries. Names are compared exactly: a space around a name, another
        letter case, an empty element, an unknown name and a type this feed
        does not carry are each `400 invalid_request`, and a repeated `types`
        parameter is refused, never merged. Leaving `types` out and sending
        `types=` are the same unfiltered query, and the order of the names
        does not matter. To leave one type out, send the list of the other
        types the feed carries. A cursor or bookmark belongs to the
        credential, the feed and the `types` list it was issued for: the same
        credential presenting it to another signal feed or with another list
        gets `400 cursor_query_mismatch`, and one issued to another credential
        is `400 invalid_request`, the same as a malformed one. Only `limit`,
        `cursor`, `bookmark` and `types` are accepted; `cursor` and `bookmark`
        together, and an unknown or repeated parameter, are `400
        invalid_request`.

        Requires `ecosystem:signals:read`, `ecosystem:accounts:read` and
        `ecosystem:partners:read`. Not yet enabled for every workspace: until
        yours is released a call answers `503 feature_disabled`.
      security:
        - ecosystemClientCredentials: [ecosystem:signals:read, ecosystem:accounts:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - name: cursor
          in: query
          required: false
          description: The `page.nextCursor` of the previous snapshot page. Never sent with `bookmark`.
          schema:
            type: string
            maxLength: 1024
        - name: bookmark
          in: query
          required: false
          description: The `bookmark` of the last snapshot page or the previous change page. Never sent with `cursor`.
          schema:
            type: string
            maxLength: 1024
        - name: types
          in: query
          required: false
          description: >-
            A comma separated list of the event types this feed carries,
            compared exactly: `overlap.created`, `overlap.updated`,
            `overlap.removed`, `partner_deal_opened`,
            `partner_deal_closed_won` and `partner_greenfield_closed_won`.
            Absent and empty are the same unfiltered query. Never repeated.
          schema:
            type: string
            maxLength: 1024
      responses:
        "200":
          description: One snapshot page or one change page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignalFeedResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "410":
          description: >-
            `cursor_expired`: the bookmark is older than the 30 day window.
            `resync_required`: the credential's view changed, the snapshot's
            position expired or the change feed's contract was updated. Either
            way, take a new snapshot through `resync.href`, which is this
            feed's own path, and replace your copy.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeFeedResyncError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /signals/leads:
    get:
      operationId: signals_leads_read
      x-cosignal-operation: signals_leads_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: released_per_workspace
      tags: [Signals]
      summary: Read the signals about your leads, as a snapshot and then a bookmark.
      description: |-
        The lead signal feed: the overlap signals (`overlap.created`,
        `overlap.updated` and `overlap.removed`) about your leads. It reads
        exactly like `GET /signals/accounts`: a snapshot whose items all carry
        `historical: true`, a bookmark that starts the change pages at the
        snapshot's cut-off, the same 30 day bookmark and `410` answers, the
        same `types` rules and the same cursor rules. A workspace member sees
        only the signals about the leads they own.

        Requires `ecosystem:signals:read`, `ecosystem:leads:read` and
        `ecosystem:partners:read`. Not yet enabled for every workspace: until
        yours is released, and while lead overlaps are not enabled for your
        workspace, a call answers `503 feature_disabled`.
      security:
        - ecosystemClientCredentials: [ecosystem:signals:read, ecosystem:leads:read, ecosystem:partners:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - name: cursor
          in: query
          required: false
          description: The `page.nextCursor` of the previous snapshot page. Never sent with `bookmark`.
          schema:
            type: string
            maxLength: 1024
        - name: bookmark
          in: query
          required: false
          description: The `bookmark` of the last snapshot page or the previous change page. Never sent with `cursor`.
          schema:
            type: string
            maxLength: 1024
        - name: types
          in: query
          required: false
          description: >-
            A comma separated list of the event types this feed carries,
            compared exactly: `overlap.created`, `overlap.updated` and
            `overlap.removed`. Absent and empty are the same unfiltered query.
            Never repeated.
          schema:
            type: string
            maxLength: 1024
      responses:
        "200":
          description: One snapshot page or one change page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignalFeedResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "410":
          description: >-
            `cursor_expired` or `resync_required`, as on `GET
            /signals/accounts`. Take a new snapshot through `resync.href`,
            which is this feed's own path.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeFeedResyncError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /signals/own-deals:
    get:
      operationId: signals_own_deals_read
      x-cosignal-operation: signals_own_deals_read
      x-cosignal-principal-restriction: none
      x-cosignal-availability: released_per_workspace
      tags: [Signals]
      summary: Read the signals about your own deals, as a snapshot and then a bookmark.
      description: |-
        The own deal signal feed: `own_deal_opened` and `own_deal_closed_won`
        for your own opportunities. It reads exactly like `GET
        /signals/accounts`: a snapshot whose items all carry `historical:
        true`, a bookmark that starts the change pages at the snapshot's
        cut-off, the same 30 day bookmark and `410` answers, the same `types`
        rules and the same cursor rules. A deal that is deleted or merged in
        your CRM produces no signal, so read the record again when you need to
        be sure. A workspace member sees only the signals about deals on the
        accounts they own.

        `attribution.updated` rides this feed as well (`AttributionEventData`
        says when it fires and what it carries): one item for a deal whose
        credit changed after attribution was recalculated, naming the credited
        partners and carrying the deal's value in the currency of your CRM.
        These items are served only to an admin principal, an integration key
        or an admin's delegated token, that also holds
        `ecosystem:attribution:read` and `ecosystem:partners:read`; for anyone
        else the feed answers as if those items did not exist, with no `403`
        and no sign of a gap, so those two scopes are not required to call
        this route. The feed never names a partner that ended the partnership
        or does not share on the API channel.

        Requires `ecosystem:signals:read` and `ecosystem:opportunities:read`.
        Not yet enabled for every workspace: until yours is released a call
        answers `503 feature_disabled`.
      security:
        - ecosystemClientCredentials: [ecosystem:signals:read, ecosystem:opportunities:read]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - name: cursor
          in: query
          required: false
          description: The `page.nextCursor` of the previous snapshot page. Never sent with `bookmark`.
          schema:
            type: string
            maxLength: 1024
        - name: bookmark
          in: query
          required: false
          description: The `bookmark` of the last snapshot page or the previous change page. Never sent with `cursor`.
          schema:
            type: string
            maxLength: 1024
        - name: types
          in: query
          required: false
          description: >-
            A comma separated list of the event types this feed carries,
            compared exactly: `own_deal_opened`, `own_deal_closed_won` and
            `attribution.updated`. Absent and empty are the same unfiltered
            query. Never repeated.
          schema:
            type: string
            maxLength: 1024
      responses:
        "200":
          description: One snapshot page or one change page.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignalFeedResponse"
        "400":
          $ref: "#/components/responses/BadRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "410":
          description: >-
            `cursor_expired` or `resync_required`, as on `GET
            /signals/accounts`. Take a new snapshot through `resync.href`,
            which is this feed's own path.
          headers:
            Cache-Control:
              $ref: "#/components/headers/NoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeFeedResyncError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
components:
  securitySchemes:
    ecosystemClientCredentials:
      type: oauth2
      description: |-
        An opaque access token bound to the API resource
        (`https://app.co-signal.com/api/v1`), presented as
        `Authorization: Bearer <token>`. A token minted for the MCP server is
        refused on audience before any right is consulted, and the legacy
        `cosignal:*` MCP scopes never grant an API right. There are two ways
        in. An admin created integration key exchanges its credentials, as
        described below. A signed in member can also get a token of their own
        through the authorization code flow and the consent screen in
        Co-Signal, for a workspace in the API rollout: the member chooses
        which of the scopes the app asks for to give. Any member can give a
        scope whose operations are open to members; the scope descriptions
        below say where a power is narrower. A scope marked below as
        an organization wide power can be given only by a current workspace
        admin, and only through an app an admin approved; a workspace that
        requires approved apps refuses an app nobody approved.
        `ecosystem:crm:write` can be given today, but the CRM command
        operations it covers answer `503 feature_disabled` until the CRM
        commands are released.

        **The token step.** `POST https://app.co-signal.com/api/oauth/token`
        with a form encoded (or JSON) body carrying
        `grant_type=client_credentials` and
        `resource=https://app.co-signal.com/api/v1`, and the key's
        credentials sent by exactly ONE of the two confidential client
        methods of RFC 6749 section 2.3.1:

        - `client_secret_basic`: an `Authorization: Basic` header whose user
          name is the key's ID and whose password is its `csk_` secret, each
          form url encoded before the base64 step as RFC 6749 requires (a key
          ID and a secret need no encoding). Most HTTP tools send this by
          default.
        - `client_secret_post`: `client_id` (the key's ID) and
          `client_secret` (the `csk_` secret, shown once when the key was
          created or rotated) as body parameters.

        A request that presents a Basic header AND a body `client_id` or
        `client_secret` is refused `400 invalid_request` before any lookup,
        whether or not the two agree (D-66); a malformed Basic header still
        counts as a Basic attempt. While the API is enabled the authorization
        server metadata lists both methods, after `none` for the MCP
        server's public clients.

        Success returns `access_token`, `token_type` of `Bearer`,
        `expires_in` of 600 and `scope`. There is NO refresh token: when the
        token expires, exchange the key again. Without a `scope` parameter
        the token carries every scope the key holds, and `scope` lists them.
        An optional, space separated `scope` is checked against the key and
        refused with `400 invalid_scope` if the key lacks one; otherwise the
        token is NARROWED to it: every request is authorized with the key's
        current scopes intersected with the requested set, so the token never
        gains a scope added to the key later, and `scope` in the answer lists
        the requested set.

        **Exchange budget (B-08).** Each verified key ID may exchange 60 times
        per fixed 60 second window. The credential is checked before the
        durable budget is spent, so an unknown key or wrong secret cannot
        create a bucket or exhaust a real key's budget. Basic and body
        requests for one verified key share the budget. An id outside the
        key ID format (1 to 128 letters and digits) is refused
        `401 invalid_client` before credential lookup. A separate fixed
        per-instance ceiling protects credential checks and can return
        `429 rate_limited` before one key reaches 60 exchanges.

        **Token step errors** are RFC 6749 shaped bodies, `{ "error": "<code>" }`
        with no `requestId`, and are a DIFFERENT vocabulary from the error
        envelope every `/api/v1` route uses:

        - `401 invalid_client`: an unknown, revoked, suspended or expired
          key, a wrong secret, a disabled workspace, or a malformed Basic
          header. These are deliberately indistinguishable. When the request
          authenticated with HTTP Basic, the 401 carries
          `WWW-Authenticate: Basic realm="co-signal"` (RFC 6749 section 5.2);
          a body authenticated 401 carries no challenge.
        - `400 invalid_scope`: a requested `scope` the key does not hold.
        - `400 invalid_target`: a `resource` that is not the API. While the
          API is switched off for everyone the token endpoint does not accept
          the API as a resource at all, so this is then the answer to every
          correctly formed request.
        - `400 unsupported_grant_type`: a client credentials request naming
          the MCP resource while the API is switched off.
        - `400 invalid_request`: a missing `client_id`, `client_secret` or
          `resource`, an unreadable body, or credentials in both the Basic
          header and the body (D-66).
        - `429 rate_limited`: over the verified key exchange budget or the
          per-instance credential check ceiling, with a `retry-after` header
          giving the seconds until the window ends.
        - `500 server_error`: the exchange budget could not be checked.

        A key revoked BETWEEN the two steps fails at the resource step as
        `401 invalid_token`, not here.
      flows:
        clientCredentials:
          tokenUrl: https://app.co-signal.com/api/oauth/token
          scopes:
            ecosystem:accounts:read: Read your own accounts and, with the partners scope, their overlaps.
            ecosystem:activity:read: Read your workspace's own co-sell request activity, newest first, with the period summary.
            ecosystem:attribution:read: Read what each partner is credited with in your stored intelligence rollup.
            ecosystem:contacts:read: Read your own contacts.
            ecosystem:crm:write: Preview and run the CRM commands (push one account's overlap fields, sync one overlap record) through an app a workspace admin switched the command on for, while the person is a workspace admin. A Co-Signal extension, released per workspace. Never creatable on an integration key.
            ecosystem:directory:read: Read the current members of your workspace. An organization wide power held by an admin created key, or by a current admin through an app an admin approved.
            ecosystem:discovery:read: Read partner suggestions from the Co-Signal directory. An organization wide power held by an admin created key, or by a current admin through an app an admin approved.
            ecosystem:drafts:write: Prepare co-sell requests for a named person to approve in Co-Signal, as an integration key. Nothing reaches a partner until that person approves it.
            ecosystem:exports:read: Create, read, cancel and download exports of your own records, with the scopes that read each family exported. An organization wide power held by a key that has the scope, or by a current admin through an app an admin approved. Released per workspace and not creatable on a key in this release.
            ecosystem:identity:read: Read who is calling and the workspace the credential belongs to.
            ecosystem:imports:write: Upload a CSV of accounts, read its dry run, apply it and cancel it, as your own imports. An organization wide power held by a key that has the scope, or by a current admin through an app an admin approved. Released per workspace and not creatable on a key in this release.
            ecosystem:intelligence:read: Read greenfield candidates, recommendations and a partner's score context, with the partners scope.
            ecosystem:knowledge:read: Search the curated knowledge corpus. Given by a member or an assistant through consent; not creatable on a key in this release.
            ecosystem:leads:read: Read and resolve your own leads.
            ecosystem:lists:read: Read the lists you may see, their links, and recompute their rows.
            ecosystem:lists:write: Create, change and delete lists, as an integration key or a workspace admin.
            ecosystem:opportunities:read: Read your own opportunities.
            ecosystem:partners:read: Read your partnerships and, with the accounts scope, account overlaps.
            ecosystem:populations:read: Read your own account and lead populations.
            ecosystem:reports:read: Read the saved reports you may see and recompute their rows.
            ecosystem:requests:act: Create co-sell requests for a named person to approve, as an integration key; accept, decline and close requests as a person through their own delegated token. A key never answers or closes a request.
            ecosystem:signals:read: Read the account, lead and own deal signal feeds, with the scopes that read each feed's records. Released per workspace.
            ecosystem:timeline:write: Add internal notes to your accounts' timelines, as an integration key or a person through an approved app. A note is never sent to a partner.
            ecosystem:webhooks:manage: Create, read, change, delete, test and rotate the secret of webhook subscriptions, with the scopes that read each event type subscribed to. An organization wide power held by a key that has the scope, or by a current admin through an app an admin approved. Released per workspace.
  headers:
    NoStore:
      description: Every response, success or error, is private and never cached.
      schema:
        type: string
        enum: [no-store]
    RetryAfter:
      description: Whole seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
    WwwAuthenticate:
      description: >-
        An RFC 6750 Bearer challenge carrying the RFC 9728 `resource_metadata`
        pointer and, on a refused token or scope, the error name.
      schema:
        type: string
    ETag:
      description: >-
        The list's version as a quoted integer, for example `"3"`. Send it
        back unchanged as `If-Match` to change or delete the list.
      schema:
        type: string
        pattern: '^"[0-9]+"$'
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: >-
        Page size, an integer from 1 to 200, default 25. A value outside 1 to
        200, or one that is not a whole number, is `400 invalid_request`,
        never a silent clamp. Changing `limit` between pages does not
        invalidate a cursor.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 25
    Cursor:
      name: cursor
      in: query
      required: false
      description: The `page.nextCursor` of the previous page. See `ListCursor`.
      schema:
        $ref: "#/components/schemas/ListCursor"
    TimelineLimit:
      name: limit
      in: query
      required: false
      description: >-
        Page size, an integer from 1 to 50, default 50. A value outside 1 to
        50, or one that is not a whole number, is `400 invalid_request`, never
        a silent clamp. Changing `limit` between pages does not invalidate a
        cursor.
      schema:
        type: integer
        minimum: 1
        maximum: 50
    AccountId:
      name: accountId
      in: path
      required: true
      description: An `acct_` public id from one of your own responses.
      schema:
        type: string
        pattern: "^acct_[A-Za-z0-9_-]+$"
    LeadId:
      name: leadId
      in: path
      required: true
      description: A `lead_` public id from one of your own responses.
      schema:
        type: string
        pattern: "^lead_[A-Za-z0-9_-]+$"
    PartnerId:
      name: partnerId
      in: path
      required: true
      description: A `ptsh_` partnership public id from `GET /partners`.
      schema:
        type: string
        pattern: "^ptsh_[A-Za-z0-9_-]+$"
    PopulationId:
      name: populationId
      in: path
      required: true
      description: >-
        A `pop_` population public id from `GET /populations`. Any other
        value answers the same `404 not_found` as an id that was never issued.
      schema:
        type: string
        pattern: "^pop_[A-Za-z0-9_-]+$"
    ReportId:
      name: reportId
      in: path
      required: true
      description: >-
        A `rpt_` report public id from `GET /reports`. Any other value, and
        a report you may not see, answers the same `404 not_found` as an id
        that was never issued.
      schema:
        type: string
        pattern: "^rpt_[A-Za-z0-9_-]+$"
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: >-
        1 to 255 visible ASCII characters you choose for this change. A retry
        with the same key and the same request answers the first result and
        changes nothing; the same key with a different request is `422
        idempotency_key_reused`. Keys are remembered for 24 hours, per key or
        person and per operation.
      schema:
        type: string
        pattern: "^[\\x21-\\x7E]{1,255}$"
    IfMatch:
      name: If-Match
      in: header
      required: true
      description: >-
        The version you read, quoted exactly as the `ETag` gave it. Without
        it the answer is `428 precondition_required`; if the list has changed
        since, `409 version_conflict` and nothing is written.
      schema:
        type: string
        pattern: '^"[0-9]+"$'
    ListId:
      name: listId
      in: path
      required: true
      description: >-
        A `list_` list public id from `GET /lists`. Any other value, and a
        list you may not see, answers the same `404 not_found` as an id that
        was never issued.
      schema:
        type: string
        pattern: "^list_[A-Za-z0-9_-]+$"
    PartnerPopulationId:
      name: partnerPopulationId
      in: path
      required: true
      description: >-
        A `ppop_` public id from `GET /partner-populations`. Any other value
        answers the same `404 not_found` as an id that was never issued.
      schema:
        type: string
        pattern: "^ppop_[A-Za-z0-9_-]+$"
    RequestId:
      name: requestId
      in: path
      required: true
      description: >-
        A `req_` public id from one of your own responses (the `result` of an
        action, or an id minted to your workspace). Any other value, and a
        request your side may not answer, answers the same `404 not_found` as
        an id that was never issued.
      schema:
        type: string
        pattern: "^req_[A-Za-z0-9_-]+$"
    ActionId:
      name: actionId
      in: path
      required: true
      description: >-
        An `act_` public id an action answered with. Any other value, and an
        action you did not start (unless you are an admin), answers the same
        `404 not_found` as an id that was never issued.
      schema:
        type: string
        pattern: "^act_[A-Za-z0-9_-]+$"
    OverlapId:
      name: overlapId
      in: path
      required: true
      description: >-
        An `aovl_` public id of one overlap pair of your own accounts, from the
        change feed's `account_overlaps` resource or from a push preview's
        overlap targets. Any other value, an id issued to another workspace and
        an id of another type answer the same `404 not_found` as an id that
        was never issued.
      schema:
        type: string
        pattern: "^aovl_[A-Za-z0-9_-]+$"
    CommandIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Required to execute a CRM command (a missing key is `400`), ignored by
        a preview. 1 to 255 visible ASCII characters you choose for this
        command. A retry with the same key and the same `payloadHash` answers
        the first result and writes nothing more; the same key with another
        hash is `422 idempotency_key_reused`.
      schema:
        type: string
        pattern: "^[\\x21-\\x7E]{1,255}$"
    ExportId:
      name: exportId
      in: path
      required: true
      description: >-
        A `job_` public id an export answered with. Any other value, an export
        of another key or workspace and an id of another type answer the same
        `404 not_found` as an id that was never issued.
      schema:
        type: string
        pattern: "^job_[A-Za-z0-9_-]+$"
    ExportPartNumber:
      name: part
      in: query
      required: true
      description: >-
        The part to download: a positive whole number of at most nine digits,
        given once, from the `number` of a part in the export's status. Any
        other value, a repeated `part` and any other query parameter are `400
        invalid_request`, judged before any export is read.
      schema:
        type: integer
        minimum: 1
        maximum: 999999999
    ImportId:
      name: importId
      in: path
      required: true
      description: >-
        A `job_` public id an import answered with. Any other value, an import
        of another key or workspace and an id of another type answer the same
        `404 not_found` as an id that was never issued.
      schema:
        type: string
        pattern: "^job_[A-Za-z0-9_-]+$"
    SubscriptionId:
      name: subscriptionId
      in: path
      required: true
      description: >-
        A `whs_` public id a webhook subscription answered with. Any other
        value, a subscription of another workspace, a deleted subscription and
        an id of another type answer the same `404 not_found` as an id that
        was never issued.
      schema:
        type: string
        pattern: "^whs_[A-Za-z0-9_-]+$"
    DeliveryId:
      name: deliveryId
      in: path
      required: true
      description: >-
        A `dlv_` public id a webhook delivery answered with (the
        `X-CoSignal-Delivery-Id` your endpoint received). Any other value, a
        delivery of another workspace and an id of another type answer the same
        `404 not_found` as an id that was never issued.
      schema:
        type: string
        pattern: "^dlv_[A-Za-z0-9_-]+$"
  responses:
    BadRequest:
      description: >-
        `invalid_request`: a malformed id, an unknown, missing or repeated
        query parameter, or an out of range value. Refused on the request
        alone, before any record is read.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    BadRequestList:
      description: >-
        `invalid_request`: an unknown or repeated query parameter, a `limit`
        outside 1 to 200, or a cursor that is malformed, was never issued or
        was issued to another workspace (all one identical answer).
        `cursor_query_mismatch`: a well formed cursor presented against a
        different query than the one that issued it.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    Unauthorized:
      description: >-
        `invalid_token`: no bearer, or an unknown, expired or revoked token,
        a token for a key revoked after it was minted, or a token minted for
        the MCP server rather than the API. One identical answer for all of
        them.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        WWW-Authenticate:
          $ref: "#/components/headers/WwwAuthenticate"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    Forbidden:
      description: >-
        `insufficient_scope`: the key lacks a scope this operation requires,
        or the caller's kind is not permitted. One identical answer for both,
        given before any record is read, so it never reveals whether a
        record exists.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        WWW-Authenticate:
          $ref: "#/components/headers/WwwAuthenticate"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    VersionConflict:
      description: >-
        `version_conflict`: the list changed since the version you sent as
        `If-Match`. Nothing was written; read it again and retry.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    IdempotencyKeyReused:
      description: >-
        `idempotency_key_reused`: this `Idempotency-Key` was already used for
        a different request. Nothing was written.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    PreconditionRequired:
      description: >-
        `precondition_required`: a change or delete must send the version
        you read as `If-Match`.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    NotFound:
      description: >-
        `not_found`: identical for an id that was never issued, an id issued
        to another workspace, a record that no longer exists and a record you
        may not see. There is no distinguishing error.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    RateLimited:
      description: >-
        `rate_limited`: over 120 requests a minute for this key or 600 a
        minute for this workspace, in fixed 60 second windows. `Retry-After`
        is the seconds until the current window ends. Keyed on who is
        calling, never on what was asked for.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    Unavailable:
      description: >-
        `feature_disabled`: the API is switched off, or your workspace is not
        on the rollout. For a valid bearer the two are byte identical by
        design, which keeps private only whether a given workspace is on the
        rollout; whether the API is switched on at all is public (see
        Availability in the document description). `temporarily_unavailable`, with
        `Retry-After`: an unexpected failure, including a response that did
        not match this contract on its way out.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    BatchLimited:
      description: >-
        `budget_exceeded`: the batch would pass the workspace's
        `batch_selectors_per_day` budget (50,000 selectors per UTC day). The
        whole batch is refused and nothing in it was resolved. The body is
        `EcosystemBudgetError`: the budget's `name`, `limit`, `used` and the
        `resetsAt` instant (00:00 UTC), and `Retry-After` is the seconds until
        then. `rate_limited`: over 120 requests a minute for this key or 600 a
        minute for this workspace, the same answer as every other route
        (`Retry-After` is the seconds until the current window ends). The two
        codes differ so a client can tell a daily budget from a burst limit.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/EcosystemBudgetError"
              - $ref: "#/components/schemas/EcosystemError"
    RequestStateConflict:
      description: >-
        `version_conflict`: the request is no longer in a state this answer
        applies to (it was already decided, or it is not accepted yet).
        Nothing was written.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    PreviewStale:
      description: >-
        `preview_stale`: the preview whose `payloadHash` you sent no longer
        holds. The payload changed, the mapping or a partner's sharing changed,
        or the record no longer qualifies; it is one answer for every cause.
        Nothing was written. Preview again and send the new hash.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    ExportLimited:
      description: >-
        `budget_exceeded`: the workspace already has 3 exports open
        (`export_concurrent_jobs`), so the export was not created and nothing
        was written. The body is `EcosystemBudgetError`: the budget's `name`,
        `limit`, `used` and a `resetsAt` of null (a slot frees when an open
        export ends), and `Retry-After` is 60 seconds. `rate_limited`: over 120
        requests a minute for this key or 600 a minute for this workspace, the
        same answer as every other route. The two codes differ so a client can
        tell a budget from a burst limit.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/EcosystemBudgetError"
              - $ref: "#/components/schemas/EcosystemError"
    ImportLimited:
      description: >-
        `budget_exceeded`: either the workspace already has 2 imports open
        (`import_concurrent_jobs`, on a create, or on an apply started while 2
        other imports are open), so the upload was not stored or nothing was
        applied, or the apply would pass the workspace's 200,000 applied rows
        per UTC day (`import_rows_per_day`), so nothing was applied. The body is
        `EcosystemBudgetError`: the budget's `name`, `limit`, `used` and a
        `resetsAt` (null for the concurrency budget, whose slot frees when an
        open import ends, and the end of the UTC day for the row budget), and
        `Retry-After` says when to try again. `rate_limited`: over 120 requests
        a minute for this key or 600 a minute for this workspace, the same
        answer as every other route. The two codes differ so a client can tell a
        budget from a burst limit.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/EcosystemBudgetError"
              - $ref: "#/components/schemas/EcosystemError"
    SubscriptionConflict:
      description: >-
        `subscription_conflict`: the workspace already has a webhook
        subscription for that URL, or already holds the ten subscriptions a
        workspace may have. One answer for both, so it never says which, and
        nothing was written.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    WebhookVersionConflict:
      description: >-
        `version_conflict`: the subscription changed while the rotation ran.
        Nothing was written; read the subscription and try again.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    WebhookTestLimited:
      description: >-
        `rate_limited`: over 120 requests a minute for this key or 600 a
        minute for this workspace, or over the 10 test calls a minute one
        workspace may make (each sends a request to your URL), in fixed 60
        second windows. `Retry-After` is the seconds until the current window
        ends. Keyed on who is calling, never on which subscription was named.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
    DeliveryNotReplayable:
      description: >-
        `delivery_not_replayable`: the delivery cannot be replayed or
        discarded. One answer for every cause (data that is no longer yours to
        receive, a subscription that is not `active` or no longer lists the
        type, a credential that no longer qualifies, a delivery that is not in
        a replayable status, or a discard of anything but a dead letter), so it
        never says which, and nothing was written.
      headers:
        Cache-Control:
          $ref: "#/components/headers/NoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EcosystemError"
  schemas:
    EcosystemError:
      type: object
      additionalProperties: false
      description: The one error envelope every `/api/v1` response uses. `requestId` is the only correlation handle.
      required: [error, requestId]
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - invalid_token
            - insufficient_scope
            - not_found
            - feature_disabled
            - rate_limited
            - temporarily_unavailable
            - cursor_query_mismatch
            - cursor_expired
            - resync_required
            - precondition_required
            - version_conflict
            - idempotency_key_reused
            - preview_stale
            - budget_exceeded
            - subscription_conflict
            - delivery_not_replayable
        requestId:
          type: string
          minLength: 16
          maxLength: 128
    FieldProvenance:
      type: object
      additionalProperties: false
      description: >-
        Where one field value came from and when. `sharing` of `absent` means
        the source asserted nothing (or a declared fallback display name was
        served); `unsupported` means the value could not be represented by
        this contract. On your own reads nothing is `withheld`.
      required: [fieldKey, source, sharing, sourceUpdatedAt, observedAt]
      properties:
        fieldKey:
          type: string
          minLength: 1
          maxLength: 200
          description: Size guard, see the document description.
        source:
          type: string
          enum: [own, partner_shared, computed]
        sharing:
          type: string
          enum: [disclosed, counts_only, withheld, stale, unsupported, absent]
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        observedAt:
          type: string
          format: date-time
    AccountSummary:
      type: object
      additionalProperties: false
      required: [id, name, domain, type, openOpportunityCount, openOpportunityValue, provenance]
      properties:
        id:
          type: string
          description: An `acct_` public id.
        name:
          type: string
          minLength: 1
          description: The stored account name, or Unnamed company when the stored name is blank.
        domain:
          type: string
          maxLength: 253
          nullable: true
        type:
          type: string
          maxLength: 64
          nullable: true
        openOpportunityCount:
          type: integer
          minimum: 0
          nullable: true
          description: A counted value, so it is never negative.
        openOpportunityValue:
          type: number
          nullable: true
          description: >-
            The SUM of the account's open opportunity amounts, so it carries
            the same rule as `OpportunitySummary.amount`: no sign constraint,
            and a negative value is valid when credits, reversals or churn
            deals outweigh the rest.
        provenance:
          type: array
          items:
            $ref: "#/components/schemas/FieldProvenance"
    PartnerContact:
      type: object
      additionalProperties: false
      required: [id, name, title, sharedContact]
      properties:
        id:
          type: string
        name:
          type: string
          minLength: 1
          maxLength: 200
        title:
          type: string
          maxLength: 200
          nullable: true
        sharedContact:
          type: boolean
          description: "Whether this partner contact is a shared contact, a person you both hold. True only when both workspaces share contacts with each other for the overlapping populations and the contact's work email matches a contact you hold at your own account; false in every other case. Computed when you read; the email itself is never returned."
    PartnerOpportunity:
      type: object
      additionalProperties: false
      required: [id, status, closeDate, amount]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [open, won, lost]
        closeDate:
          type: string
          format: date-time
          nullable: true
        amount:
          type: number
          minimum: 0
    PartnerContext:
      type: object
      additionalProperties: false
      required: [id, partnershipId, name, domain, type, relationship, tier, score, reason, customFields, withheld]
      properties:
        id:
          type: string
          description: >-
            The partner's matching ACCOUNT, as an `acct_` public id. It names
            the partner's account record, not the partner workspace, whose
            display name is `name`. The pairing is kept as shipped, because
            renaming a live member would break callers.
        partnershipId:
          type: string
          description: The `ptsh_` partnership id.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The partner WORKSPACE's display name, not the name of the account `id` names.
        domain:
          type: string
          maxLength: 253
          nullable: true
        type:
          type: string
          maxLength: 64
          nullable: true
        relationship:
          type: string
          maxLength: 64
          nullable: true
        tier:
          type: string
          minLength: 1
          maxLength: 32
        score:
          type: number
          minimum: 0
        reason:
          type: string
          maxLength: 300
        contacts:
          type: array
          description: OMITTED when your key lacks `ecosystem:contacts:read` or the partner withholds contacts.
          items:
            $ref: "#/components/schemas/PartnerContact"
        opportunities:
          type: array
          description: OMITTED when your key lacks `ecosystem:opportunities:read` or the partner withholds opportunities.
          items:
            $ref: "#/components/schemas/PartnerOpportunity"
        customFields:
          type: array
          description: >-
            Always present, possibly empty (D-60, D-28). It holds exactly the
            account fields the partner's owner chose for their CRM sync and
            ticked by key on the sharing rule, above counts, that covers this
            account, and only while that owner has switched Automation on for
            the partnership. The channel of the call decides the destination
            use; a rule's stored destination list no longer narrows or widens
            anything. Empty whenever nothing is disclosed, with no marker.
          items:
            $ref: "#/components/schemas/PartnerCustomField"
        withheld:
          type: array
          description: Section names omitted from this entry, never a protected identifier.
          items:
            type: string
            maxLength: 64
    PartnerCustomField:
      type: object
      additionalProperties: false
      description: >-
        One partner custom field value, as the partner's CRM holds it. `key`
        is the field's key in the partner's catalog, `label` its display
        label, `valueType` the catalog type, and `value` the stored value
        coerced to that type, or null: a `date` is an ISO 8601 date-time
        string, a `number` a finite number, a `boolean` a boolean, and a
        `string` or `enum` a trimmed string of up to 4000 characters (a
        longer one is served as null). `provenance` says where the value
        came from.
      required: [key, label, valueType, value, provenance]
      properties:
        key:
          type: string
          minLength: 1
          maxLength: 200
        label:
          type: string
          minLength: 1
          maxLength: 255
        valueType:
          type: string
          enum: [string, number, boolean, date, enum]
        value:
          nullable: true
          oneOf:
            - type: string
              maxLength: 4000
            - type: number
            - type: boolean
        provenance:
          $ref: "#/components/schemas/ValueProvenance"
    ValueProvenance:
      type: object
      additionalProperties: false
      description: >-
        Where one disclosed value came from. `provider` is the CRM that
        asserted it, `sourceField` the provider's own field name, or null
        when no name is known, `sourceUpdatedAt` when the provider last
        changed it, or null when the provider gives no per value time,
        `observedAt` when Co-Signal observed it, and `mappingVersion` the
        field catalog mapping that produced the key.
      required: [provider, sourceField, sourceUpdatedAt, observedAt, mappingVersion]
      properties:
        provider:
          type: string
          enum: [salesforce, hubspot, csv]
        sourceField:
          type: string
          maxLength: 200
          nullable: true
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        observedAt:
          type: string
          format: date-time
        mappingVersion:
          type: string
          minLength: 1
          maxLength: 32
    AccountContextData:
      type: object
      additionalProperties: false
      required: [account, partners]
      properties:
        account:
          $ref: "#/components/schemas/AccountSummary"
        partners:
          type: array
          description: Record level partner context only. A counts only partner contributes nothing.
          items:
            $ref: "#/components/schemas/PartnerContext"
    AccountContextMeta:
      type: object
      additionalProperties: false
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        limitations:
          type: array
          description: Free text on this route, unlike the closed `ListLimitation` vocabulary of the list feeds.
          items:
            type: string
            maxLength: 200
    AccountContextResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/AccountContextData"
        meta:
          $ref: "#/components/schemas/AccountContextMeta"
    ListCursor:
      type: string
      maxLength: 512
      description: >-
        An opaque bookmark. Pass `page.nextCursor` back unchanged as `cursor`
        to get the next page. It is base64url encoded and READABLE, not
        encrypted, signed or secret, and it carries no authorization
        decision: every page is authorized again from scratch. It is bound
        to the query that issued it, so presenting it with a different query
        is `400 cursor_query_mismatch`; a malformed cursor, one that was
        never issued and one issued to another workspace are all the same
        `400 invalid_request`. If the row a cursor points at has since left
        the feed, the next page continues from the next row after it. Pages
        are not a snapshot (`pages_are_not_a_snapshot`).
    ListPage:
      type: object
      additionalProperties: false
      description: >-
        `hasMore` is true exactly when `nextCursor` is a string. Exhaustion is
        `hasMore` false with a null `nextCursor`. A page cut short by a
        route's own budget while rows remain is `hasMore` true and carries
        `results_capped_by_request_budget`; `GET /leads/resolve` is the one
        exception, capped with `hasMore` false because it has no cursor.
      required: [nextCursor, hasMore]
      properties:
        nextCursor:
          type: string
          maxLength: 512
          nullable: true
          description: A `ListCursor`, or null on the last page.
        hasMore:
          type: boolean
    ListLimitation:
      type: string
      description: |-
        A closed vocabulary of facts about a response, never free text:

        - `order_is_stable_not_alphabetical`: the feed order is a stable
          internal order and is not a name sort.
        - `total_count_unavailable`: no exact total is returned, because
          permission safe counting does not exist in this release.
        - `pages_are_not_a_snapshot`: rows may be added, removed or unshared
          between pages and are not reconciled.
        - `results_capped_by_request_budget`: the route stopped short of the
          requested page size for its own cost bound, which is different
          from exhaustion.
        - `no_crm_source_timestamp`: at least one returned row has no CRM
          source timestamp, or the resource has none (the overlaps, partners,
          identity and account context reads always declare it), so the null
          `sourceUpdatedAt` is a fact rather than a gap.
        - `overlap_recomputed_per_request`: overlaps are computed for this
          request rather than read from a cache, so `computedAt` is the
          computation moment.
        - `record_field_values_on_detail_only`: a list row carries identity
          and provenance; the field values are on THIS resource's detail
          response. True for leads, and only leads, in this release.
        - `filters_and_sorts_unsupported`: the route accepts no filter or sort
          predicate, stated so a caller never assumes an ignored parameter
          filtered anything.
        - `chosen_field_values_not_served`: your workspace has chosen custom
          fields for this object type and no read in this release serves
          their values for this resource, so their absence is a product
          boundary rather than an empty CRM. Emitted by the opportunities
          feed. Never used interchangeably with
          `record_field_values_on_detail_only`.
        - `rows_withheld_unservable`: one or more rows of this change feed
          page were left out because a stored value cannot be served within
          the response limits; each is sent once a later change makes it
          servable. Emitted only by `GET /api/v1/changes`.
        - `partner_channel_settings_apply`: rows that read partner shared
          data follow each partner's own channel settings for the API, so a
          partner who has not switched on automation for your partnership
          contributes no rows here. Present on every report and list
          response, whether or not anything was withheld.
        - `explanations_limited_to_first_rows`: a request that sent
          `explain=true` got an explanation on at most the first five rows of
          the page, and every later row of the page carries
          `explanation: null`. Emitted only by `GET /api/v1/greenfield`,
          `GET /api/v1/recommendations` and `GET /api/v1/suggestions`.
        - `attribution_from_stored_rollup`: the figures of this response come
          from the stored intelligence rollup, so no partner is listed until a
          rollup has been produced for your workspace. Present on every
          `GET /api/v1/attribution` response, whether or not a row was served.
        - `partner_scores_admin_view_only`: partner scores are an admin view,
          so a member's response never carries a partner score or a score
          ordering. Present on every member response of
          `GET /api/v1/accounts/{accountId}/overlapping-partners`, whether or
          not any partner has a score.
      enum:
        - order_is_stable_not_alphabetical
        - total_count_unavailable
        - pages_are_not_a_snapshot
        - results_capped_by_request_budget
        - no_crm_source_timestamp
        - overlap_recomputed_per_request
        - record_field_values_on_detail_only
        - filters_and_sorts_unsupported
        - chosen_field_values_not_served
        - rows_withheld_unservable
        - partner_channel_settings_apply
        - explanations_limited_to_first_rows
        - attribution_from_stored_rollup
        - partner_scores_admin_view_only
    ListMeta:
      type: object
      additionalProperties: false
      description: >-
        The freshness and limitation block of every list and detail response
        except the account context route. `sourceUpdatedAt` is the latest CRM
        edit time among the rows returned on the reads of your own accounts,
        contacts, opportunities and leads, with `no_crm_source_timestamp`
        declared when a returned row has none; every other feed leaves it null
        (the overlaps, partners, identity and account context reads declare
        that limitation, and the change feed declares none because its rows
        carry their own time). `sourceFreshness` is present on those same
        reads, except the two batch reads, and states each connected CRM's
        measured freshness.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
          description: When Co-Signal observed the data.
        computedAt:
          type: string
          format: date-time
          description: When this answer was calculated.
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
          description: When the source last changed the data, or null.
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        sourceFreshness:
          type: array
          description: >-
            One entry for each connected CRM, sorted by `provider`, and an
            empty list when none is connected; a disconnected CRM is not
            listed. Present on the reads of your own accounts, contacts,
            opportunities and leads (not on `POST /accounts/batch` or
            `POST /leads/batch`), and left out (never defaulted) when the
            state could not be read for the request.
          items:
            $ref: "#/components/schemas/SourceFreshness"
    SourceFreshness:
      type: object
      additionalProperties: false
      description: >-
        One connected CRM's measured source freshness, derived when the
        answer is built from stored state and never stored. `staleAfterSeconds`
        is the bound for its `cadence`: the larger of three poll intervals
        and 600 seconds for `incremental`, and 25200 for `scheduled_full` and
        `demo`.
      required: [provider, state, reason, cadence, lastSuccessAt, staleAfterSeconds]
      properties:
        provider:
          type: string
          enum: [salesforce, hubspot]
        state:
          type: string
          enum: [fresh, stale, degraded]
          description: >-
            `fresh` is earned: the last successful refresh is within
            `staleAfterSeconds` and nothing has failed since. `stale` means no
            successful refresh yet, or an older one. `degraded` outranks both,
            however recent the last success, and a degraded connection is
            never shown as fresh.
        reason:
          type: string
          nullable: true
          enum: [quota, entitlement, authorization, provider_error, reconciliation_guard, null]
          description: >-
            Set exactly when `state` is `degraded`, otherwise null:
            `quota` (the CRM's request allowance is nearly spent),
            `entitlement` (the edition or connected user lacks access to an
            object), `authorization` (the connection must be reconnected),
            `provider_error` (the CRM failed) or `reconciliation_guard` (a
            deletion of more than 20 percent of an object or 1,000 records was
            held for a person to review).
        cadence:
          type: string
          enum: [incremental, scheduled_full, demo]
        lastSuccessAt:
          type: string
          format: date-time
          nullable: true
          description: When the last successful refresh finished, or null when there has been none.
        staleAfterSeconds:
          type: integer
          minimum: 1
    AccountListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AccountSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    BatchSelector:
      type: object
      additionalProperties: false
      minProperties: 1
      description: >-
        One selector of a batch: the object `GET /accounts/resolve` (or
        `GET /leads/resolve`) takes as its query, exactly one selector per
        object. A company `domain`; the pair `source` plus `sourceRecordId`
        (either half alone names nothing); a company or person `name`; or, on
        `POST /leads/batch` only, an own work `email`. `source` is `salesforce`
        or `hubspot` on `POST /accounts/batch`, and may also be `csv` on
        `POST /leads/batch`. An object that is not exactly one valid selector
        for the route is an `invalid` item, never a failed batch.
      properties:
        domain:
          type: string
          minLength: 1
          maxLength: 253
        name:
          type: string
          minLength: 1
          maxLength: 500
        source:
          type: string
          enum: [salesforce, hubspot, csv]
        sourceRecordId:
          type: string
          minLength: 1
          maxLength: 256
        email:
          type: string
          minLength: 3
          maxLength: 320
    BatchResolveRequest:
      type: object
      additionalProperties: false
      description: >-
        The body of `POST /accounts/batch` and `POST /leads/batch`: a list of
        1 to 100 selectors. Any other key, an empty list or a longer one is
        `400 invalid_request`.
      required: [selectors]
      properties:
        selectors:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: "#/components/schemas/BatchSelector"
    AccountBatchItem:
      type: object
      additionalProperties: false
      description: >-
        The answer for one selector. `index` is its position in the request.
        `candidates` and `limitations` are what `GET /accounts/resolve` returns
        as `data` and `meta.limitations` for that selector alone. `error` is
        present only on an `invalid` item, whose `candidates` and `limitations`
        are empty.
      required: [index, status, candidates, limitations]
      properties:
        index:
          type: integer
          minimum: 0
          maximum: 99
        status:
          type: string
          enum: [matched, ambiguous, none, invalid]
        error:
          type: string
          enum: [invalid_request]
        candidates:
          type: array
          items:
            $ref: "#/components/schemas/AccountSummary"
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
    AccountBatchResponse:
      type: object
      additionalProperties: false
      description: >-
        One item per selector in input order, with the freshness block. There
        is no `page`: a batch is a lookup with nothing to continue from.
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/AccountBatchItem"
        meta:
          $ref: "#/components/schemas/ListMeta"
    BudgetRefusal:
      type: object
      additionalProperties: false
      description: >-
        The numbers of the budget a request would have passed. `resetsAt` is
        the next 00:00 UTC for a day budget and null for a concurrency budget,
        which frees a slot when an open job ends. It says nothing about the
        request that hit the budget.
      required: [name, limit, used, resetsAt]
      properties:
        name:
          type: string
          enum:
            - export_rows_per_day
            - export_concurrent_jobs
            - import_rows_per_day
            - import_concurrent_jobs
            - batch_selectors_per_day
        limit:
          type: integer
          minimum: 1
        used:
          type: integer
          minimum: 0
        resetsAt:
          type: string
          format: date-time
          nullable: true
    EcosystemBudgetError:
      type: object
      additionalProperties: false
      description: >-
        The `429 budget_exceeded` body: the error envelope plus the refused
        budget. A different code from `rate_limited`, which stays the per
        minute limiter's.
      required: [error, requestId, budget]
      properties:
        error:
          type: string
          enum: [budget_exceeded]
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        budget:
          $ref: "#/components/schemas/BudgetRefusal"
    ExportCreateRequest:
      type: object
      additionalProperties: false
      description: >-
        The body of `POST /exports`: the families to export, each once, and an
        optional format. Any other key, an unknown or repeated family, an empty
        list or a `csv` export of more than one family is `400
        invalid_request`.
      required: [families]
      properties:
        families:
          type: array
          minItems: 1
          maxItems: 6
          items:
            type: string
            enum:
              - accounts
              - contacts
              - opportunities
              - leads
              - account_overlaps
              - lead_overlaps
        format:
          type: string
          enum: [ndjson, csv]
          description: Defaults to `ndjson`. A `csv` export carries exactly one family.
    ExportPart:
      type: object
      additionalProperties: false
      description: >-
        One downloadable part of a complete export. It names no storage path.
        `sha256` is the hex digest of the part's bytes, also its `ETag`.
      required: [number, family, rowCount, byteSize, sha256, contentType]
      properties:
        number:
          type: integer
          minimum: 1
        family:
          type: string
          enum:
            - accounts
            - contacts
            - opportunities
            - leads
            - account_overlaps
            - lead_overlaps
        rowCount:
          type: integer
          minimum: 0
        byteSize:
          type: integer
          minimum: 0
        sha256:
          type: string
          pattern: "^[0-9a-f]{64}$"
        contentType:
          type: string
          minLength: 1
          maxLength: 128
    ExportAccountLine:
      type: object
      additionalProperties: false
      description: >-
        One line of an NDJSON export part of the accounts family: the family's
        list row, which carries its own id.
      required: [family, data]
      properties:
        family:
          type: string
          enum: [accounts]
        data:
          $ref: "#/components/schemas/AccountSummary"
    ExportContactLine:
      type: object
      additionalProperties: false
      description: >-
        One line of an NDJSON export part of the contacts family: the family's
        list row, which carries its own id.
      required: [family, data]
      properties:
        family:
          type: string
          enum: [contacts]
        data:
          $ref: "#/components/schemas/ContactSummary"
    ExportOpportunityLine:
      type: object
      additionalProperties: false
      description: >-
        One line of an NDJSON export part of the opportunities family: the family's
        list row, which carries its own id.
      required: [family, data]
      properties:
        family:
          type: string
          enum: [opportunities]
        data:
          $ref: "#/components/schemas/OpportunitySummary"
    ExportLeadLine:
      type: object
      additionalProperties: false
      description: >-
        One line of an NDJSON export part of the leads family: the family's
        list row, which carries its own id.
      required: [family, data]
      properties:
        family:
          type: string
          enum: [leads]
        data:
          $ref: "#/components/schemas/LeadSummary"
    ExportAccountOverlapLine:
      type: object
      additionalProperties: false
      description: >-
        One line of an NDJSON export part of the account_overlaps family. `id` is the
        opaque id of the overlap pair that `GET /changes` gives the same pair
        (an upsert and a tombstone carry it), so a tombstone can be matched to
        the exported row. It holds neither the partnership nor your own
        record's id.
      required: [family, id, data]
      properties:
        family:
          type: string
          enum: [account_overlaps]
        id:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        data:
          $ref: "#/components/schemas/AccountOverlapPair"
    ExportLeadOverlapLine:
      type: object
      additionalProperties: false
      description: >-
        One line of an NDJSON export part of the lead_overlaps family. `id` is the
        opaque id of the overlap pair that `GET /changes` gives the same pair
        (an upsert and a tombstone carry it), so a tombstone can be matched to
        the exported row. It holds neither the partnership nor your own
        record's id.
      required: [family, id, data]
      properties:
        family:
          type: string
          enum: [lead_overlaps]
        id:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        data:
          $ref: "#/components/schemas/LeadOverlapPair"
    ExportLine:
      description: >-
        One line of an NDJSON export part, discriminated by `family`: the
        `data` of a line is always the row of its own family.
      oneOf:
        - $ref: "#/components/schemas/ExportAccountLine"
        - $ref: "#/components/schemas/ExportContactLine"
        - $ref: "#/components/schemas/ExportOpportunityLine"
        - $ref: "#/components/schemas/ExportLeadLine"
        - $ref: "#/components/schemas/ExportAccountOverlapLine"
        - $ref: "#/components/schemas/ExportLeadOverlapLine"
      discriminator:
        propertyName: family
        mapping:
          accounts: "#/components/schemas/ExportAccountLine"
          contacts: "#/components/schemas/ExportContactLine"
          opportunities: "#/components/schemas/ExportOpportunityLine"
          leads: "#/components/schemas/ExportLeadLine"
          account_overlaps: "#/components/schemas/ExportAccountOverlapLine"
          lead_overlaps: "#/components/schemas/ExportLeadOverlapLine"
    ExportStatusBudget:
      type: object
      additionalProperties: false
      description: >-
        The budget a failed export's refusal named: its name, its limit and,
        for a day budget, when it resets.
      required: [name, limit, resetsAt]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 64
        limit:
          type: integer
          minimum: 0
        resetsAt:
          type: string
          format: date-time
          nullable: true
    ExportStatusFailure:
      type: object
      additionalProperties: false
      description: >-
        Present on a `failed` export only. `code` is a closed code:
        `budget_exceeded` (the day's rows, with `budget` named),
        `export_too_large` (the export would pass 2,000 parts or a list of
        parts too large to store; it is not retried and its parts are
        deleted) or `internal`. `budget` is present only for a budget
        refusal.
      required: [code]
      properties:
        code:
          type: string
          enum: [budget_exceeded, internal, export_too_large]
        budget:
          $ref: "#/components/schemas/ExportStatusBudget"
    ExportStatus:
      type: object
      additionalProperties: false
      description: >-
        The status of one export. A cancelled, revoked, failed or expired
        export lists no part and no bookmark, and a revoked export gives no
        reason. A `complete` export whose 24 hour window has passed reads
        `expired` even before the cleanup runs.
      required:
        - id
        - status
        - families
        - format
        - createdAt
        - completedAt
        - expiresAt
        - watermark
        - rowCount
        - countsByFamily
        - parts
        - bookmarks
      properties:
        id:
          type: string
          pattern: "^job_[A-Za-z0-9_-]+$"
        status:
          type: string
          enum: [queued, running, complete, failed, cancelled, revoked, expired]
        families:
          type: array
          items:
            type: string
            enum:
              - accounts
              - contacts
              - opportunities
              - leads
              - account_overlaps
              - lead_overlaps
        format:
          type: string
          enum: [ndjson, csv]
        createdAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            The time the export finished: the time of the commit that
            completed it, after its last part was written. Null until the
            export completes. The 24 hour download window counts from it.
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            The end of the 24 hour download window; null until the export
            completes. Export files are kept for 24 hours after the export
            completes.
        watermark:
          type: string
          format: date-time
          description: The snapshot high watermark the export was taken at.
        rowCount:
          type: integer
          minimum: 0
        countsByFamily:
          type: object
          additionalProperties:
            type: integer
            minimum: 0
        parts:
          type: array
          items:
            $ref: "#/components/schemas/ExportPart"
        bookmarks:
          type: object
          description: >-
            One change feed bookmark for each family of a `complete` export,
            keyed by family; empty for every other status.
          additionalProperties:
            type: string
            maxLength: 4096
        error:
          $ref: "#/components/schemas/ExportStatusFailure"
    ExportStatusResponse:
      type: object
      additionalProperties: false
      description: The answer of `GET /exports/{exportId}` and `POST /exports/{exportId}/cancel`.
      required: [export]
      properties:
        export:
          $ref: "#/components/schemas/ExportStatus"
    ExportCreatedResponse:
      type: object
      additionalProperties: false
      description: >-
        The `202` answer of `POST /exports`: the export's status (queued) and
        the `act_` id of the action that follows it at `GET /actions/{actionId}`.
      required: [export, actionId]
      properties:
        export:
          $ref: "#/components/schemas/ExportStatus"
        actionId:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
    ImportMapping:
      type: object
      additionalProperties: false
      description: >-
        The columns of your file, by header. `name` and `domain` are required;
        a blank string for an optional column means it is absent. `crmRecordId`
        needs `crmSource`. Any other key is `400 invalid_request`.
      required: [name, domain]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 500
        domain:
          type: string
          minLength: 1
          maxLength: 500
        type:
          type: string
          minLength: 1
          maxLength: 500
        ownerEmail:
          type: string
          minLength: 1
          maxLength: 500
        crmRecordId:
          type: string
          minLength: 1
          maxLength: 500
        crmSource:
          type: string
          enum: [salesforce, hubspot]
    ImportPrecedence:
      type: object
      additionalProperties: false
      description: >-
        The default is that the CRM wins and the import fills gaps. Naming a
        field here lets the import win for that field only. The owner column
        cannot be named: it never outranks a CRM owner.
      required: [importWinsFields]
      properties:
        importWinsFields:
          type: array
          maxItems: 2
          items:
            type: string
            enum: [name, type]
    ImportCreateOptions:
      type: object
      additionalProperties: false
      description: >-
        The `options` field of `POST /imports`, sent as JSON text. There is no
        channel key: an import is an internal write attributed to the key or
        person that made it.
      required: [mapping]
      properties:
        mapping:
          $ref: "#/components/schemas/ImportMapping"
        precedence:
          $ref: "#/components/schemas/ImportPrecedence"
    ImportUploadRequest:
      type: object
      additionalProperties: false
      description: >-
        The multipart body of `POST /imports`: exactly these two fields, each
        once. The file is read with a byte cap while it streams, so a body over
        25 MiB (plus a small allowance for the multipart framing) is refused
        without being held, whatever length it declares.
      required: [file, options]
      properties:
        file:
          type: string
          format: binary
          description: The CSV, at most 25 MiB and 100,000 data rows, with a header row.
        options:
          $ref: "#/components/schemas/ImportCreateOptions"
    ImportReportCounts:
      type: object
      additionalProperties: false
      description: Exact counts of the rows the report covers, by outcome. A repeat of an earlier row is counted as a skip.
      required: [total, creates, updates, skips, conflicts, errors]
      properties:
        total:
          type: integer
          minimum: 0
        creates:
          type: integer
          minimum: 0
        updates:
          type: integer
          minimum: 0
        skips:
          type: integer
          minimum: 0
        conflicts:
          type: integer
          minimum: 0
        errors:
          type: integer
          minimum: 0
    ImportRowError:
      type: object
      additionalProperties: false
      description: >-
        One row that could not be used (or, for `duplicate_in_file`, a repeat
        that was skipped): its 1 based data row number, the role of the field and
        a closed code. Never the cell's value.
      required: [row, field, code]
      properties:
        row:
          type: integer
          minimum: 1
        field:
          type: string
          enum: [name, domain, type, ownerEmail, crmRecordId, row]
        code:
          type: string
          enum:
            - missing_name
            - invalid_domain
            - personal_email_domain
            - invalid_type
            - invalid_owner_email
            - missing_identity
            - duplicate_in_file
            - ambiguous_crm_reference
            - identity_mismatch
            - value_too_long
    ImportRowConflict:
      type: object
      additionalProperties: false
      description: >-
        One row whose file value differs from a value the import may not
        overwrite: the row number and the fields in conflict. Never a value.
      required: [row, fields]
      properties:
        row:
          type: integer
          minimum: 1
        fields:
          type: array
          minItems: 1
          maxItems: 3
          items:
            type: string
            enum: [name, type, ownerEmail]
    ImportReport:
      type: object
      additionalProperties: false
      description: >-
        What a dry run or an apply reports: exact counts, and at most 200 row
        errors and 200 conflicts as samples. The counts are always exact.
      required: [counts, errors, conflicts]
      properties:
        counts:
          $ref: "#/components/schemas/ImportReportCounts"
        errors:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/ImportRowError"
        conflicts:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/ImportRowConflict"
    ImportStatusFailure:
      type: object
      additionalProperties: false
      description: >-
        Present on a `failed` import only, as its closed `code`:
        `too_many_rows`, `invalid_file`, `file_unavailable` or `internal`.
      required: [code]
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 64
    ImportStatus:
      type: object
      additionalProperties: false
      description: >-
        The status of one import. A cancelled or revoked import gives no
        reason. The report of an apply is what it applied before it stopped.
      required:
        - id
        - status
        - fileName
        - createdAt
        - dryRunCompletedAt
        - appliedAt
        - expiresAt
        - dryRun
        - apply
        - error
      properties:
        id:
          type: string
          pattern: "^job_[A-Za-z0-9_-]+$"
        status:
          type: string
          enum: [validating, dry_run_complete, applying, applied, failed, cancelled, revoked, expired]
        fileName:
          type: string
          minLength: 1
          maxLength: 255
          description: The uploaded file's name as you sent it, trimmed and bounded. It is never a path.
        createdAt:
          type: string
          format: date-time
        dryRunCompletedAt:
          type: string
          format: date-time
          nullable: true
        appliedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            When the file of an unapplied dry run expires (7 days after the dry
            run finished); null in every other status.
        dryRun:
          allOf:
            - $ref: "#/components/schemas/ImportReport"
          nullable: true
          description: The dry run's report once it finished; null before.
        apply:
          allOf:
            - $ref: "#/components/schemas/ImportReport"
          nullable: true
          description: The apply's report so far; null until an apply starts.
        error:
          allOf:
            - $ref: "#/components/schemas/ImportStatusFailure"
          nullable: true
    ImportStatusResponse:
      type: object
      additionalProperties: false
      description: The answer of `GET /imports/{importId}` and `POST /imports/{importId}/cancel`.
      required: [import]
      properties:
        import:
          $ref: "#/components/schemas/ImportStatus"
    ImportCreatedResponse:
      type: object
      additionalProperties: false
      description: >-
        The `202` answer of `POST /imports` (the dry run was queued) and of
        `POST /imports/{importId}/apply` (the apply was started): the import's
        status and the `act_` id of that call's own action, followed at
        `GET /actions/{actionId}`.
      required: [import, actionId]
      properties:
        import:
          $ref: "#/components/schemas/ImportStatus"
        actionId:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"

    AccountDetailResponse:
      type: object
      additionalProperties: false
      description: >-
        One account, in the accounts list's own row shape, with the same
        freshness block as every other detail response and no `page`: a detail
        names one record rather than a page of them.
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/AccountSummary"
        meta:
          $ref: "#/components/schemas/ListMeta"
    LeadOwner:
      type: object
      additionalProperties: false
      description: The owning member's display name and email, never a user id.
      required: [name, email]
      properties:
        name:
          type: string
          maxLength: 500
          nullable: true
          description: Null when the owner has left the workspace.
        email:
          type: string
          maxLength: 320
          nullable: true
          description: Null when the owner has left the workspace.
    LeadConversion:
      type: object
      additionalProperties: false
      description: Present when the lead has converted. The lead's own id is never rewritten.
      required: [accountId, contactId, convertedAt]
      properties:
        accountId:
          type: string
          description: The `acct_` id of the account it converted into.
        contactId:
          type: string
          nullable: true
          description: The `cont_` id of the contact it converted into, when there is one.
        convertedAt:
          type: string
          format: date-time
    LeadSummary:
      type: object
      additionalProperties: false
      description: >-
        One lead. `personName`, `title`, `personEmail` and `owner` may be
        OMITTED; on your own read nothing is withheld, so an omitted key
        means the lead does not have that value. There is no CRM record id:
        use `GET /leads/resolve` to go from a CRM reference to a lead.
      required: [id, companyName, companyDomain, status, source, conversion, provenance]
      properties:
        id:
          type: string
          description: A `lead_` public id.
        companyName:
          type: string
          minLength: 1
          maxLength: 500
          description: Size guard, see the document description. A blank stored name is served as `Unnamed company`.
        companyDomain:
          type: string
          maxLength: 253
          nullable: true
        personName:
          type: string
          minLength: 1
          maxLength: 500
          description: OMITTED when the lead names no person.
        title:
          type: string
          maxLength: 500
          nullable: true
          description: OMITTED when withheld; null when the source asserted no title. Nothing is withheld on your own read.
        personEmail:
          type: string
          maxLength: 320
          nullable: true
          description: OMITTED when withheld; null when the source asserted no email. Nothing is withheld on your own read.
        owner:
          allOf:
            - $ref: "#/components/schemas/LeadOwner"
          description: >-
            OMITTED when the lead has no owner. On your own read that absence
            means the lead has no owner rather than that an owner was
            withheld, because nothing is withheld on your own read. The first
            partner lead read will need a nullable form of this key or its
            own withheld marker before the two readings could be confused.
        status:
          type: string
          maxLength: 255
          nullable: true
          description: The CRM's own status value, unconstrained. Size guard, see the document description.
        source:
          type: string
          enum: [salesforce, hubspot, csv]
        conversion:
          allOf:
            - $ref: "#/components/schemas/LeadConversion"
          nullable: true
        provenance:
          type: array
          items:
            $ref: "#/components/schemas/FieldProvenance"
    LeadFieldValue:
      type: object
      additionalProperties: false
      description: >-
        One field value on the lead detail response, catalog or chosen
        custom field. `source` names the CRM that asserted the value,
        `sourceField` the CRM's own field name (null when none is known), and
        `mappingVersion` the field catalog mapping that produced the key.
      required: [fieldKey, value, source, sourceField, sourceUpdatedAt, observedAt, mappingVersion]
      properties:
        fieldKey:
          type: string
          maxLength: 200
          description: A catalog key such as `lead.industry`, or a chosen key such as `lead.custom.region`. Size guard.
        value:
          nullable: true
          oneOf:
            - type: string
              maxLength: 4000
            - type: number
            - type: boolean
        source:
          type: string
          enum: [salesforce, hubspot, csv]
        sourceField:
          type: string
          maxLength: 200
          nullable: true
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        observedAt:
          type: string
          format: date-time
        mappingVersion:
          type: string
          minLength: 1
          maxLength: 32
    LeadListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/LeadSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    LeadBatchItem:
      type: object
      additionalProperties: false
      description: >-
        The answer for one selector of `POST /leads/batch`. `index` is its
        position in the request. `candidates` and `limitations` are what
        `GET /leads/resolve` returns as `data` and `meta.limitations` for that
        selector alone. `error` is present only on an `invalid` item, whose
        `candidates` and `limitations` are empty.
      required: [index, status, candidates, limitations]
      properties:
        index:
          type: integer
          minimum: 0
          maximum: 99
        status:
          type: string
          enum: [matched, ambiguous, none, invalid]
        error:
          type: string
          enum: [invalid_request]
        candidates:
          type: array
          items:
            $ref: "#/components/schemas/LeadSummary"
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
    LeadBatchResponse:
      type: object
      additionalProperties: false
      description: >-
        One item per selector in input order, with the freshness block, and no
        `page`.
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/LeadBatchItem"
        meta:
          $ref: "#/components/schemas/ListMeta"
    LeadDetail:
      type: object
      additionalProperties: false
      description: Every `LeadSummary` key plus `fields`.
      required: [id, companyName, companyDomain, status, source, conversion, provenance, fields]
      properties:
        id:
          type: string
        companyName:
          type: string
          minLength: 1
          maxLength: 500
        companyDomain:
          type: string
          maxLength: 253
          nullable: true
        personName:
          type: string
          minLength: 1
          maxLength: 500
        title:
          type: string
          maxLength: 500
          nullable: true
        personEmail:
          type: string
          maxLength: 320
          nullable: true
        owner:
          allOf:
            - $ref: "#/components/schemas/LeadOwner"
          description: OMITTED when the lead has no owner, exactly as on `LeadSummary`.
        status:
          type: string
          maxLength: 255
          nullable: true
        source:
          type: string
          enum: [salesforce, hubspot, csv]
        conversion:
          allOf:
            - $ref: "#/components/schemas/LeadConversion"
          nullable: true
        provenance:
          type: array
          items:
            $ref: "#/components/schemas/FieldProvenance"
        fields:
          type: array
          items:
            $ref: "#/components/schemas/LeadFieldValue"
    LeadDetailData:
      type: object
      additionalProperties: false
      required: [lead, withheld]
      properties:
        lead:
          $ref: "#/components/schemas/LeadDetail"
        withheld:
          type: array
          description: Section names your key or grant does not cover. Always empty on your own read.
          items:
            type: string
            maxLength: 64
    LeadDetailResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/LeadDetailData"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ContactSummary:
      type: object
      additionalProperties: false
      description: One of your own contacts. A contact carries no chosen custom field.
      required: [id, accountId, name, email, title, source, provenance]
      properties:
        id:
          type: string
          description: A `cont_` public id.
        accountId:
          type: string
          description: The `acct_` id of the contact's account.
        name:
          type: string
          minLength: 1
          maxLength: 500
          description: Size guard. A blank stored name is served as `Unknown contact`.
        email:
          type: string
          maxLength: 320
          nullable: true
        title:
          type: string
          maxLength: 500
          nullable: true
        source:
          type: string
          enum: [salesforce, hubspot, csv]
        provenance:
          type: array
          items:
            $ref: "#/components/schemas/FieldProvenance"
    ContactListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ContactSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    OpportunitySummary:
      type: object
      additionalProperties: false
      required: [id, accountId, name, amount, stage, status, closeDate, source, provenance]
      properties:
        id:
          type: string
          description: An `opp_` public id.
        accountId:
          type: string
          description: The `acct_` id of the deal's account.
        name:
          type: string
          minLength: 1
          maxLength: 500
          description: Size guard. A blank stored name is served as `Untitled deal`.
        amount:
          type: number
          description: >-
            A finite number with NO sign constraint. The sign is the CRM's
            own: a negative amount is a valid credit, reversal or churn deal,
            not a defect.
        stage:
          type: string
          maxLength: 255
          description: The CRM's own stage value, unconstrained. Size guard.
        status:
          type: string
          enum: [open, won, lost]
        closeDate:
          type: string
          format: date-time
          nullable: true
        source:
          type: string
          enum: [salesforce, hubspot, csv]
        provenance:
          type: array
          items:
            $ref: "#/components/schemas/FieldProvenance"
    OpportunityListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/OpportunitySummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PartnerWorkspace:
      type: object
      additionalProperties: false
      description: The partner company.
      required: [id, name, domain]
      properties:
        id:
          type: string
          description: A `ws_` public id.
        name:
          type: string
          maxLength: 500
        domain:
          type: string
          maxLength: 253
          nullable: true
    PartnerSummary:
      type: object
      additionalProperties: false
      required: [id, workspace, status, nickname, createdAt, tags]
      properties:
        id:
          type: string
          description: The `ptsh_` PARTNERSHIP public id.
        workspace:
          $ref: "#/components/schemas/PartnerWorkspace"
        status:
          type: string
          enum: [active]
        nickname:
          type: string
          maxLength: 500
          nullable: true
        createdAt:
          type: string
          format: date-time
        tags:
          type: array
          description: >-
            The tags your workspace applied to this partnership, sorted by
            name; always present, possibly empty. Never the partner's own tags.
          items:
            $ref: "#/components/schemas/PartnerTagRef"
    PartnerTag:
      type: object
      additionalProperties: false
      description: One of your workspace's own partner tags.
      required: [id, name, createdAt, updatedAt]
      properties:
        id:
          type: string
          description: A `ptag_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 30
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    PartnerTagRef:
      type: object
      additionalProperties: false
      description: A tag as a partner row names it, with the same `ptag_` id the tag list serves.
      required: [id, name]
      properties:
        id:
          type: string
          description: A `ptag_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 30
    PartnerTagListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerTag"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PartnerListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PartnerDetailResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/PartnerSummary"
        meta:
          $ref: "#/components/schemas/ListMeta"
    MePrincipalDelegatedUser:
      type: object
      additionalProperties: false
      description: A signed in person.
      required: [kind, id, displayName, role]
      properties:
        kind:
          type: string
          enum: [delegated_user]
        id:
          type: string
          description: A `mem_` public id, minted to the credential's workspace.
        displayName:
          type: string
          minLength: 1
          maxLength: 500
        role:
          type: string
          enum: [admin, member]
          description: The current effective role, read fresh on every call.
    MePrincipalIntegration:
      type: object
      additionalProperties: false
      description: An integration key, named by its label and nothing else.
      required: [kind, name]
      properties:
        kind:
          type: string
          enum: [integration]
        name:
          type: string
          minLength: 1
          maxLength: 200
    MeWorkspace:
      type: object
      additionalProperties: false
      description: The one workspace the credential belongs to.
      required: [id, name, domain]
      properties:
        id:
          type: string
          description: A `ws_` public id.
        name:
          type: string
          maxLength: 500
        domain:
          type: string
          maxLength: 253
          nullable: true
    Me:
      type: object
      additionalProperties: false
      required: [principal, workspace, scopes]
      properties:
        principal:
          oneOf:
            - $ref: "#/components/schemas/MePrincipalDelegatedUser"
            - $ref: "#/components/schemas/MePrincipalIntegration"
          discriminator:
            propertyName: kind
            mapping:
              delegated_user: "#/components/schemas/MePrincipalDelegatedUser"
              integration: "#/components/schemas/MePrincipalIntegration"
        workspace:
          $ref: "#/components/schemas/MeWorkspace"
        scopes:
          type: array
          description: The scopes the credential holds, sorted.
          items:
            type: string
            maxLength: 64
    MeResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/Me"
        meta:
          $ref: "#/components/schemas/ListMeta"
    WorkspaceSummary:
      type: object
      additionalProperties: false
      description: The one workspace the credential belongs to.
      required: [id, name, domain]
      properties:
        id:
          type: string
          description: A `ws_` public id, the same one `GET /api/v1/me` returns.
        name:
          type: string
          maxLength: 500
        domain:
          type: string
          maxLength: 253
          nullable: true
    WorkspaceListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    WorkspaceMember:
      type: object
      additionalProperties: false
      description: The minimum projection of one current member.
      required: [id, displayName, email, role, joinedAt]
      properties:
        id:
          type: string
          description: A `mem_` public id, the same one `GET /api/v1/me` returns for that person.
        displayName:
          type: string
          minLength: 1
          maxLength: 500
        email:
          type: string
          maxLength: 320
          description: The member's work email.
        role:
          type: string
          enum: [admin, member]
          description: The member's stored role in this workspace.
        joinedAt:
          type: string
          format: date-time
    WorkspaceMemberListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceMember"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PopulationFilterCondition:
      type: object
      additionalProperties: false
      required: [field, operator, value]
      properties:
        field:
          type: string
          maxLength: 64
        fieldKey:
          type: string
          maxLength: 200
          description: Present only when the condition targets a chosen custom field.
        operator:
          type: string
          enum: [equals, contains, greater_than, less_than, before, after]
        value:
          description: Your own saved value. A stored string value carries no length bound at all.
          oneOf:
            - type: string
            - type: number
    PopulationSummary:
      type: object
      additionalProperties: false
      required: [id, name, resourceType, isDefaultTemplate, createdAt, updatedAt]
      properties:
        id:
          type: string
          description: A `pop_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 200
        resourceType:
          type: string
          enum: [account, lead]
          description: Whether this is an account population or a lead population.
        isDefaultTemplate:
          type: string
          enum: [customers, prospects, openOpportunities]
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        filter:
          type: array
          minItems: 1
          description: The saved definition. OMITTED for a caller that does not hold the admin definition view.
          items:
            $ref: "#/components/schemas/PopulationFilterCondition"
    PopulationListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PopulationSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PopulationDetailResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/PopulationSummary"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PartnerPopulationOverlap:
      type: object
      additionalProperties: false
      description: >-
        Your own overlap with this population: the number of distinct
        records of your own of the population's kind that overlap it
        (accounts for an account population, leads for a lead population),
        from the last stored computation. When your partner shares only
        counts, a count from 1 to 4 is shown as suppressed (`count` null,
        `suppressed` true); zero is a real 0.
      required: [count, suppressed, computedAt]
      properties:
        count:
          type: integer
          minimum: 0
          nullable: true
        suppressed:
          type: boolean
        computedAt:
          type: string
          format: date-time
          nullable: true
          description: The latest stored computation that contributed, null when none was ever stored.
    PartnerPopulationPartner:
      type: object
      additionalProperties: false
      description: The partnership and partner company the population arrives through, as `GET /partners` names them.
      required: [partnershipId, workspace]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership public id.
        workspace:
          $ref: "#/components/schemas/PartnerWorkspace"
    PartnerPopulation:
      type: object
      additionalProperties: false
      description: >-
        One population a partner shares with you, as a fixed projection. It
        never carries the partner's definition, the population's size, the
        partner's own population id or who changed the share.
      required: [id, name, resourceType, level, partner]
      properties:
        id:
          type: string
          description: A `ppop_` public id, issued to your workspace only.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The name your partner gave the population.
        resourceType:
          type: string
          enum: [account, lead]
        level:
          type: string
          enum: [counts, overlapping, all]
          description: The sharing level your partner granted your workspace.
        partner:
          $ref: "#/components/schemas/PartnerPopulationPartner"
        overlap:
          description: >-
            OMITTED for a lead population while lead overlaps are not
            released for your workspace; always present for an account
            population.
          allOf:
            - $ref: "#/components/schemas/PartnerPopulationOverlap"
    PartnerPopulationListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPopulation"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    PartnerPopulationDetailResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/PartnerPopulation"
        meta:
          $ref: "#/components/schemas/ListMeta"
    AccountOverlapAccount:
      type: object
      additionalProperties: false
      description: Your own account.
      required: [id, name, domain]
      properties:
        id:
          type: string
          description: An `acct_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 500
        domain:
          type: string
          maxLength: 253
          nullable: true
    AccountOverlapPartner:
      type: object
      additionalProperties: false
      required: [partnershipId, workspace, tier, score, reason]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership id.
        workspace:
          $ref: "#/components/schemas/PartnerWorkspace"
        tier:
          type: string
          enum: [exact, strong, possible]
        score:
          type: number
          minimum: 0
        reason:
          type: string
          maxLength: 300
        relationship:
          type: string
          maxLength: 64
          description: The partner's relationship to the account. OMITTED when the partner withholds that field group.
        partnerHasOpenOpportunity:
          type: boolean
          description: OMITTED when the partner withholds that field group; false means shared and there is none.
    AccountOverlapSummary:
      type: object
      additionalProperties: false
      required: [account, partners]
      properties:
        account:
          $ref: "#/components/schemas/AccountOverlapAccount"
        partners:
          type: array
          description: Empty until a partner's owner switches the automation channel on for the partnership.
          items:
            $ref: "#/components/schemas/AccountOverlapPartner"
    AccountOverlapListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AccountOverlapSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    OverlappingPartnerRow:
      type: object
      additionalProperties: false
      description: One partnership that overlaps the account in the path.
      required: [partner, tags]
      properties:
        partner:
          $ref: "#/components/schemas/AccountOverlapPartner"
        tags:
          type: array
          maxItems: 40
          description: Your workspace's own private tags on this partnership, sorted by name then id.
          items:
            $ref: "#/components/schemas/PartnerTagRef"
        score:
          type: number
          description: >-
            The partner's overall score from the stored score snapshot. Admin
            view only: OMITTED for a member and when no score is stored.
    OverlappingPartnersResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/OverlappingPartnerRow"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    AccountOverlapSearchPartnerPopulation:
      type: object
      additionalProperties: false
      description: A population your partner shares with you whose overlap cell holds this pair.
      required: [id, name]
      properties:
        id:
          type: string
          description: A `ppop_` public id, the same id `GET /partner-populations` issues to your workspace.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The name your partner gave the population.
    AccountOverlapSearchPartner:
      type: object
      additionalProperties: false
      description: One partnership's entry on a search row, the overlap feed's entry plus the partner's shared populations and shared fields.
      required: [partnershipId, workspace, tier, score, reason, partnerPopulations, customFields]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership id.
        workspace:
          $ref: "#/components/schemas/PartnerWorkspace"
        tier:
          type: string
          enum: [exact, strong, possible]
        score:
          type: number
          minimum: 0
        reason:
          type: string
          maxLength: 300
        relationship:
          type: string
          maxLength: 64
          description: The partner's relationship to the account. OMITTED when the partner withholds that field group.
        partnerHasOpenOpportunity:
          type: boolean
          description: OMITTED when the partner withholds that field group; false means shared and there is none.
        partnerPopulations:
          type: array
          description: The populations the partner shares with you that hold this pair.
          items:
            $ref: "#/components/schemas/AccountOverlapSearchPartnerPopulation"
        customFields:
          type: array
          description: The fields the partner shares with you on this channel, each with its provenance. Empty for every reason a field is not shared.
          items:
            $ref: "#/components/schemas/PartnerCustomField"
    AccountOverlapSearchRow:
      type: object
      additionalProperties: false
      required: [account, partners]
      properties:
        account:
          $ref: "#/components/schemas/AccountOverlapAccount"
        partners:
          type: array
          description: Only the entries that satisfy every filter; never empty on a returned row.
          items:
            $ref: "#/components/schemas/AccountOverlapSearchPartner"
    AccountOverlapSearchResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AccountOverlapSearchRow"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ReportPredicate:
      type: object
      additionalProperties: false
      description: One saved predicate, the shape `GET /overlaps/accounts/search` accepts.
      required: [field, operator, value]
      properties:
        field:
          type: string
          minLength: 1
          maxLength: 200
        operator:
          type: string
          enum: [equals, contains, greater_than, less_than, before, after]
        value:
          oneOf:
            - type: string
              maxLength: 4000
            - type: number
    ReportDefinition:
      type: object
      additionalProperties: false
      description: >-
        A report's saved definition. Every id is a public id issued to your
        own workspace; a filter the report does not set is null, never
        omitted.
      required: [partnershipId, populationId, partnerPopulationId, partnerTagId, match, matchedType, where, sort, columns]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership the report reads.
        populationId:
          type: string
          nullable: true
          description: A `pop_` population of your own, or null.
        partnerPopulationId:
          type: string
          nullable: true
          description: A `ppop_` population the partner shares with you, or null.
        partnerTagId:
          type: string
          nullable: true
          description: A `ptag_` partner tag, or null.
        match:
          type: string
          enum: [company_match, same_person]
          nullable: true
        matchedType:
          type: string
          enum: [lead, account]
          nullable: true
        where:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/ReportPredicate"
        sort:
          type: string
          minLength: 1
          maxLength: 201
          nullable: true
        columns:
          type: array
          maxItems: 20
          items:
            type: string
            minLength: 1
            maxLength: 200
    ReportPartner:
      type: object
      additionalProperties: false
      description: The partnership the report reads and the partner workspace's name.
      required: [id, name]
      properties:
        id:
          type: string
          description: A `ptsh_` public id.
        name:
          type: string
          maxLength: 500
    ReportSummary:
      type: object
      additionalProperties: false
      required: [id, name, view, partner, shared, createdByYou, createdAt, updatedAt]
      properties:
        id:
          type: string
          description: A `rpt_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 60
        view:
          type: string
          enum: [accounts, leads]
        partner:
          $ref: "#/components/schemas/ReportPartner"
        shared:
          type: boolean
          description: Whether every member of your workspace may open the report.
        createdByYou:
          type: boolean
          description: True only for the signed in person who created it; always false for an integration key.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ReportDetail:
      type: object
      additionalProperties: false
      required: [id, name, view, partner, shared, createdByYou, createdAt, updatedAt, definition, link]
      properties:
        id:
          type: string
          description: A `rpt_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 60
        view:
          type: string
          enum: [accounts, leads]
        partner:
          $ref: "#/components/schemas/ReportPartner"
        shared:
          type: boolean
        createdByYou:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        definition:
          $ref: "#/components/schemas/ReportDefinition"
        link:
          $ref: "#/components/schemas/AppLink"
    ReportListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ReportSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ReportDetailResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/ReportDetail"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ReportAccountRowsResponse:
      type: object
      additionalProperties: false
      description: An accounts report's rows, exactly the account overlap search rows.
      required: [view, data, page, meta]
      properties:
        view:
          type: string
          enum: [accounts]
        data:
          type: array
          items:
            $ref: "#/components/schemas/AccountOverlapSearchRow"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ReportLeadRowsResponse:
      type: object
      additionalProperties: false
      description: A leads report's rows, exactly the lead overlap rows.
      required: [view, data, page, meta]
      properties:
        view:
          type: string
          enum: [leads]
        data:
          type: array
          items:
            $ref: "#/components/schemas/LeadOverlapSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ReportRowsResponse:
      description: One page of a report's rows, discriminated by the report's `view`.
      oneOf:
        - $ref: "#/components/schemas/ReportAccountRowsResponse"
        - $ref: "#/components/schemas/ReportLeadRowsResponse"
      discriminator:
        propertyName: view
        mapping:
          accounts: "#/components/schemas/ReportAccountRowsResponse"
          leads: "#/components/schemas/ReportLeadRowsResponse"
    AppLink:
      type: string
      maxLength: 2048
      pattern: "^https?://[^\\s/?#]+/links/[a-z]+_[A-Za-z0-9_-]+$"
      description: >-
        The authenticated Co-Signal link to a list or report: the app origin,
        `/links/` and the public id already in the same response, for example
        `https://app.co-signal.com/links/list_...`. It carries no token,
        signature or query, so it grants nothing by itself: it opens the list
        or report in the app only for a signed in member of your workspace
        who may see it, checked when it is opened. There is no public or
        anonymous share link.
      example: https://app.co-signal.com/links/list_2c7XqfR0hBv1cGmTt0wQ4A
    ListFilters:
      type: object
      additionalProperties: false
      description: >-
        A list's saved filters. Every id is a public id issued to your own
        workspace; an empty array means no filter on that dimension, never
        "matches nothing".
      required: [populationIds, partnershipIds, relationshipTypes]
      properties:
        populationIds:
          type: array
          description: Your own `pop_` populations; an account in any of them qualifies.
          items:
            type: string
        partnershipIds:
          type: array
          description: The `ptsh_` partnerships whose marks count.
          items:
            type: string
        relationshipTypes:
          type: array
          description: The relationship columns that count.
          items:
            type: string
            enum: [customer, prospect, open_opportunity]
    ListSummary:
      type: object
      additionalProperties: false
      required: [id, name, shared, createdAt, updatedAt]
      properties:
        id:
          type: string
          description: A `list_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 60
        shared:
          type: boolean
          description: Whether every member of your workspace may open the list.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ListDetail:
      type: object
      additionalProperties: false
      required: [id, name, shared, createdAt, updatedAt, filters, link, version]
      properties:
        id:
          type: string
          description: A `list_` public id.
        name:
          type: string
          minLength: 1
          maxLength: 60
        shared:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        filters:
          $ref: "#/components/schemas/ListFilters"
        link:
          $ref: "#/components/schemas/AppLink"
        version:
          type: integer
          minimum: 0
          description: >-
            Bumped by one on every change; send it as `If-Match` (the `ETag`)
            to change or delete the list. A list last changed before version
            numbers existed reads as 0.
    ListWriteFilters:
      type: object
      additionalProperties: false
      description: The filters of a list you create or change; an omitted dimension is no filter.
      properties:
        populationIds:
          type: array
          maxItems: 100
          items:
            type: string
          description: "`pop_` ids of your own populations."
        partnershipIds:
          type: array
          maxItems: 100
          items:
            type: string
          description: "`ptsh_` ids of your partnerships."
        relationshipTypes:
          type: array
          items:
            type: string
            enum: [customer, prospect, open_opportunity]
    ListCreateRequest:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          maxLength: 200
          description: Trimmed, then 1 to 60 characters.
        filters:
          $ref: "#/components/schemas/ListWriteFilters"
    ListUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          maxLength: 200
          description: Trimmed, then 1 to 60 characters.
        filters:
          $ref: "#/components/schemas/ListWriteFilters"
        shared:
          type: boolean
          description: Whether every member of your workspace may open the list.
    ListRowAccount:
      type: object
      additionalProperties: false
      description: One account of your own.
      required: [id, name, domain, type]
      properties:
        id:
          type: string
          description: An `acct_` public id.
        name:
          type: string
          minLength: 1
        domain:
          type: string
          maxLength: 253
          nullable: true
        type:
          type: string
          maxLength: 64
          nullable: true
    ListRowPartner:
      type: object
      additionalProperties: false
      description: One partner mark in a relationship column.
      required: [id, name]
      properties:
        id:
          type: string
          description: The `ptsh_` partnership.
        name:
          type: string
          maxLength: 500
          description: The partner workspace's name.
    ListRow:
      type: object
      additionalProperties: false
      description: >-
        One account of your own and the partners that place it in each
        relationship column, the columns the list page in the app shows
        (Customer of, Prospect of, Open opportunity for). A row always carries
        at least one partner mark.
      required: [account, ownerName, populationNames, customerOf, prospectOf, openOpportunityFor]
      properties:
        account:
          $ref: "#/components/schemas/ListRowAccount"
        ownerName:
          type: string
          nullable: true
          description: The account owner's name from your CRM, or null.
        populationNames:
          type: array
          description: The names of the list's populations the account falls in; empty when the list has no population filter.
          items:
            type: string
            maxLength: 200
        customerOf:
          type: array
          items:
            $ref: "#/components/schemas/ListRowPartner"
        prospectOf:
          type: array
          items:
            $ref: "#/components/schemas/ListRowPartner"
        openOpportunityFor:
          type: array
          items:
            $ref: "#/components/schemas/ListRowPartner"
    ListListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ListSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ListDetailResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/ListDetail"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ListRowsResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ListRow"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    GreenfieldFactor:
      type: object
      additionalProperties: false
      description: One factor of a candidate's rank.
      required: [name, contribution, explanation]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The factor's name, one of those defined in `meta.factors` when the stored rank carries it.
        contribution:
          type: number
          description: What this factor added to the rank.
        explanation:
          type: string
          maxLength: 1000
          description: The stored sentence that explains the contribution.
    GreenfieldIcp:
      type: object
      additionalProperties: false
      description: The candidate's ideal customer profile fit.
      required: [tier, score]
      properties:
        tier:
          type: string
          enum: [strong, good, weak, insufficient]
        score:
          type: number
    GreenfieldExplanationBody:
      type: object
      additionalProperties: false
      required: [summary, leadingFactorId, evidenceFactorIds]
      properties:
        summary:
          type: string
          minLength: 1
          maxLength: 500
        leadingFactorId:
          type: string
          maxLength: 160
          nullable: true
          description: The factor the summary leads with, a slug of the factor's name (`ICP fit` is `icp_fit`); not a record id. Null when no evidence was available.
        evidenceFactorIds:
          type: array
          maxItems: 10
          items:
            type: string
            minLength: 1
            maxLength: 160
          description: The factors the summary rests on, by the same slugs. Empty when no evidence was available.
    GreenfieldExplanation:
      type: object
      additionalProperties: false
      description: >-
        A short phrasing of one row's own factors, never a new number, name or
        order. `source` is `model` only when every check against the computed
        result passed; otherwise it is `deterministic_fallback` and
        `fallbackReason` names the closed reason.
      required: [schemaVersion, source, fallbackReason, explanation]
      properties:
        schemaVersion:
          type: string
          minLength: 1
          maxLength: 64
        source:
          type: string
          enum: [model, deterministic_fallback]
        fallbackReason:
          type: string
          maxLength: 160
          nullable: true
        explanation:
          $ref: "#/components/schemas/GreenfieldExplanationBody"
    GreenfieldCandidate:
      type: object
      additionalProperties: false
      required: [id, name, domain, rank, icp, commercialPotential, factors]
      properties:
        id:
          type: string
          description: A `gfc_` public id minted to your workspace.
        name:
          type: string
          minLength: 1
          maxLength: 500
        domain:
          type: string
          maxLength: 253
          nullable: true
        rank:
          type: number
          description: The stored deterministic rank; the order of the page.
        icp:
          $ref: "#/components/schemas/GreenfieldIcp"
        commercialPotential:
          type: number
        factors:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/GreenfieldFactor"
        explanation:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/GreenfieldExplanation"
          description: >-
            Present only on a request that sent `explain=true`: the first five
            rows of the page carry it and every later row carries null.
    GreenfieldFactorDefinition:
      type: object
      additionalProperties: false
      required: [name, maxContribution, definition]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        maxContribution:
          type: number
          description: The most this factor can contribute to a rank.
        definition:
          type: string
          minLength: 1
          maxLength: 500
    GreenfieldMeta:
      type: object
      additionalProperties: false
      description: >-
        The list freshness block plus the request's own `direction` and
        `mode`, the computed `state` and the factor definitions. `state` is
        `available` when the computed candidate set is not empty and
        `no_candidates` otherwise, whatever the cause.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations, direction, mode, state, factors]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        direction:
          type: string
          enum: [partner_to_us, us_to_partner]
        mode:
          type: string
          enum: [top_prospects, recent_partner_customer]
        state:
          type: string
          enum: [available, no_candidates]
        factors:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/GreenfieldFactorDefinition"
    GreenfieldResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/GreenfieldCandidate"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/GreenfieldMeta"
    ContextPartner:
      type: object
      additionalProperties: false
      description: One partner, named by its partnership.
      required: [id, name]
      properties:
        id:
          type: string
          description: The partnership's `ptsh_` public id.
        name:
          type: string
          description: The partner workspace's name.
    PartnerContextDimension:
      type: object
      additionalProperties: false
      description: One score dimension.
      required: [key, name, measured, value, weight, contribution, shareOfScore]
      properties:
        key:
          type: string
          enum: [relationship, engagement, pipeline, revenue, coverage]
        name:
          type: string
          minLength: 1
          maxLength: 64
        measured:
          type: boolean
          description: False when nothing was measured for this dimension in the period.
        value:
          type: number
          nullable: true
          description: The dimension's value from 0 to 100, or null when it was not measured.
        weight:
          type: number
          nullable: true
          description: The share of the score this dimension carries (the weights of the five sum to one).
        contribution:
          type: number
          nullable: true
          description: The value times the weight, or null when it was not measured.
        shareOfScore:
          type: number
          nullable: true
          description: The contribution divided by `denominators.shareOfScore`, or null when either is absent or the denominator is zero.
    PartnerContextWindow:
      type: object
      additionalProperties: false
      description: >-
        The span the score's metrics cover. `start` is inclusive and `end` is
        exclusive, in ISO dates. Both are null when the stored period is not a
        span the API can state.
      required: [start, end]
      properties:
        start:
          type: string
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
          nullable: true
        end:
          type: string
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
          nullable: true
    PartnerContextDefinitions:
      type: object
      additionalProperties: false
      description: What each score dimension measures, the same on every response.
      required: [relationship, engagement, pipeline, revenue, coverage]
      properties:
        relationship:
          type: string
          minLength: 1
          maxLength: 500
        engagement:
          type: string
          minLength: 1
          maxLength: 500
        pipeline:
          type: string
          minLength: 1
          maxLength: 500
        revenue:
          type: string
          minLength: 1
          maxLength: 500
        coverage:
          type: string
          minLength: 1
          maxLength: 500
    PartnerContextDenominators:
      type: object
      additionalProperties: false
      description: >-
        What every ratio of the context is divided by. A ratio whose
        denominator is zero or absent is null.
      required: [shareOfScore, evidenceCoverage]
      properties:
        shareOfScore:
          type: number
          nullable: true
          description: The total of the measured contributions; null when no dimension was measured. A stored zero is stated as zero and makes every share null.
        evidenceCoverage:
          type: integer
          minimum: 1
          description: The number of score dimensions, the denominator of `evidenceCoverage` on the context.
    PartnerScoreContext:
      type: object
      additionalProperties: false
      required: [partner, score, band, previousScore, evidenceBand, period, comparisonPeriod, freshness, weightVersion, window, dimensions, evidenceCoverage, definitions, denominators]
      properties:
        partner:
          $ref: "#/components/schemas/ContextPartner"
        score:
          type: number
          nullable: true
          description: The partner's displayed score from 0 to 100, or null (never zero) when nothing was measured.
        band:
          type: string
          enum: [Leading, Established, Developing, Insufficient evidence]
        previousScore:
          type: number
          nullable: true
          description: The displayed score of the comparison period, when one was stored.
        evidenceBand:
          type: string
          enum: [full, limited]
          description: "`full` when all five dimensions were measured, otherwise `limited`."
        period:
          type: string
          maxLength: 200
        comparisonPeriod:
          type: string
          maxLength: 200
        freshness:
          type: string
          maxLength: 200
          nullable: true
          description: When the metrics behind the score were last fresh.
        weightVersion:
          type: string
          maxLength: 200
          description: The version of the weights this score was computed with.
        window:
          $ref: "#/components/schemas/PartnerContextWindow"
        dimensions:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/PartnerContextDimension"
        evidenceCoverage:
          type: number
          nullable: true
          description: The share of the score dimensions that were measured, from 0 to 1.
        definitions:
          $ref: "#/components/schemas/PartnerContextDefinitions"
        denominators:
          $ref: "#/components/schemas/PartnerContextDenominators"
        explanation:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/GreenfieldExplanation"
          description: Present only on a request that sent `explain=true`.
    PartnerContextMeta:
      type: object
      additionalProperties: false
      description: >-
        The list freshness block plus `emptyReason`, null on a served context
        and `member_admin_view_only` on a delegated member's empty answer.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations, emptyReason]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
          description: When the metrics behind the score were last fresh, when that is a full date and time.
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        emptyReason:
          type: string
          enum: [member_admin_view_only]
          nullable: true
    PartnerContextResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/PartnerScoreContext"
          description: The context, or null exactly when `meta.emptyReason` names why it is empty.
        meta:
          $ref: "#/components/schemas/PartnerContextMeta"
    RecommendationFactor:
      type: object
      additionalProperties: false
      description: One factor of a recommendation's score.
      required: [key, name, contribution, explanation]
      properties:
        key:
          type: string
          minLength: 1
          maxLength: 64
          description: The factor's fixed key, for example `deal_value`.
        name:
          type: string
          minLength: 1
          maxLength: 200
        contribution:
          type: number
          description: What this factor added to the score.
        explanation:
          type: string
          description: The evidence sentence behind the contribution.
    RecommendationAccount:
      type: object
      additionalProperties: false
      description: One of your own accounts.
      required: [id, name, domain]
      properties:
        id:
          type: string
          description: An `acct_` public id.
        name:
          type: string
        domain:
          type: string
          maxLength: 253
          nullable: true
    RecommendationOpportunity:
      type: object
      additionalProperties: false
      description: One of your own open opportunities.
      required: [id, name, amount, stage, closeDate]
      properties:
        id:
          type: string
          description: An `opp_` public id.
        name:
          type: string
        amount:
          type: number
        stage:
          type: string
        closeDate:
          type: string
          description: The stored expected close date.
    RecommendationWindow:
      type: object
      additionalProperties: false
      description: >-
        The span the evidence covers. `start` is the day it was generated
        (inclusive); `end` is the day it expires (exclusive), or null when it
        has no stored expiry.
      required: [start, end]
      properties:
        start:
          type: string
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
          nullable: true
        end:
          type: string
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
          nullable: true
    Recommendation:
      type: object
      additionalProperties: false
      required: [id, partner, account, opportunity, score, band, action, whyNow, factors, evidenceCount, evidenceUpdatedAt, window]
      properties:
        id:
          type: string
          description: A `rec_` public id minted to your workspace.
        partner:
          $ref: "#/components/schemas/ContextPartner"
        account:
          $ref: "#/components/schemas/RecommendationAccount"
        opportunity:
          $ref: "#/components/schemas/RecommendationOpportunity"
        score:
          type: number
          description: The Deal Navigator score from 0 to 100; the order of the page.
        band:
          type: string
          enum: [High, Medium, Low]
        action:
          type: string
          description: The suggested next step, for example a request for an introduction.
        whyNow:
          type: string
        factors:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/RecommendationFactor"
        evidenceCount:
          type: integer
          minimum: 0
          description: The number of stored evidence records behind the row.
        evidenceUpdatedAt:
          type: string
          maxLength: 64
          description: When that evidence was last updated.
        window:
          $ref: "#/components/schemas/RecommendationWindow"
        explanation:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/GreenfieldExplanation"
          description: >-
            Present only on a request that sent `explain=true`: the first five
            rows of the page carry it and every later row carries null.
    RecommendationDefinitions:
      type: object
      additionalProperties: false
      description: What each part of a recommendation means, the same on every response.
      required: [score, factors, evidence, window]
      properties:
        score:
          type: string
          minLength: 1
          maxLength: 500
        factors:
          type: string
          minLength: 1
          maxLength: 500
        evidence:
          type: string
          minLength: 1
          maxLength: 500
        window:
          type: string
          minLength: 1
          maxLength: 500
    RecommendationsMeta:
      type: object
      additionalProperties: false
      description: The list freshness block plus the definitions of a recommendation.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations, definitions]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        definitions:
          $ref: "#/components/schemas/RecommendationDefinitions"
    RecommendationsResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Recommendation"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/RecommendationsMeta"
    AttributionWindow:
      type: object
      additionalProperties: false
      description: >-
        The span the rollup covers: `start` inclusive and `end` exclusive, in
        ISO dates (both null when the stored period is not a span the API can
        state).
      required: [start, end]
      properties:
        start:
          type: string
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
          nullable: true
        end:
          type: string
          pattern: "^\\d{4}-\\d{2}-\\d{2}$"
          nullable: true
    AttributionDenominators:
      type: object
      additionalProperties: false
      description: What each ratio of the row is divided by.
      required: [primaryShare]
      properties:
        primaryShare:
          type: integer
          minimum: 0
          description: The credited opportunity count; when it is zero, `primaryShare` is null.
    AttributionPartner:
      type: object
      additionalProperties: false
      description: One partner's stored attribution rollup row.
      required: [partner, opportunityCount, primaryOpportunityCount, primaryShare, attributedValue, period, window, updatedAt, denominators]
      properties:
        partner:
          $ref: "#/components/schemas/ContextPartner"
        opportunityCount:
          type: integer
          minimum: 0
          description: The opportunities this partner is credited on in the stored rollup.
        primaryOpportunityCount:
          type: integer
          minimum: 0
          description: How many of those it is the primary credit for.
        primaryShare:
          type: number
          nullable: true
          description: The primary count divided by the credited count, or null when the credited count is zero.
        attributedValue:
          type: number
          description: The summed value of the credited opportunities.
        period:
          type: string
          minLength: 1
          maxLength: 64
          description: The rollup period, for example `6m`.
        window:
          $ref: "#/components/schemas/AttributionWindow"
        updatedAt:
          type: string
          minLength: 1
          maxLength: 64
          description: When the rollup row was produced.
        denominators:
          $ref: "#/components/schemas/AttributionDenominators"
    AttributionDefinitions:
      type: object
      additionalProperties: false
      description: What each part of an attribution row means and where the figures come from, the same on every response.
      required: [opportunityCount, primaryOpportunityCount, attributedValue, window, source]
      properties:
        opportunityCount:
          type: string
          minLength: 1
          maxLength: 500
        primaryOpportunityCount:
          type: string
          minLength: 1
          maxLength: 500
        attributedValue:
          type: string
          minLength: 1
          maxLength: 500
        window:
          type: string
          minLength: 1
          maxLength: 500
        source:
          type: string
          minLength: 1
          maxLength: 500
    AttributionMeta:
      type: object
      additionalProperties: false
      description: >-
        The list freshness block plus the definitions, and `emptyReason`, null
        on a served page and `member_admin_view_only` on a delegated member's
        empty answer.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations, definitions, emptyReason]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
          description: When the rollup row behind the newest row of the page was produced, or null for an empty page.
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        definitions:
          $ref: "#/components/schemas/AttributionDefinitions"
        emptyReason:
          type: string
          enum: [member_admin_view_only]
          nullable: true
    AttributionResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AttributionPartner"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/AttributionMeta"
    ActivityEventRequest:
      type: object
      additionalProperties: false
      description: The co-sell request an event is about.
      required: [id, type, status]
      properties:
        id:
          type: string
          description: A `req_` public id minted to your workspace.
        type:
          type: string
          enum: [intro, context]
        status:
          type: string
          enum: [requested, accepted, declined, closed]
          description: The request's status now, which may be later than the event.
    ActivityEvent:
      type: object
      additionalProperties: false
      description: One co-sell request event of your own workspace.
      required: [id, type, occurredAt, partner, request]
      properties:
        id:
          type: string
          description: An `evt_` public id minted to your workspace.
        type:
          type: string
          enum: [request_sent, request_answered]
        occurredAt:
          type: string
          format: date-time
          description: The stored time your workspace sent the request, or accepted or declined it.
        partner:
          $ref: "#/components/schemas/ContextPartner"
        request:
          $ref: "#/components/schemas/ActivityEventRequest"
    ActivityPartnerCount:
      type: object
      additionalProperties: false
      description: One partner's counts in the window.
      required: [partner, sent, answered]
      properties:
        partner:
          $ref: "#/components/schemas/ContextPartner"
        sent:
          type: integer
          minimum: 0
        answered:
          type: integer
          minimum: 0
    ActivitySummary:
      type: object
      additionalProperties: false
      description: The Performance tab's own figures for the window, without a per member breakdown.
      required: [requestsSent, requestsAnswered, accountsInfluenced, byPartner]
      properties:
        requestsSent:
          type: integer
          minimum: 0
        requestsAnswered:
          type: integer
          minimum: 0
        accountsInfluenced:
          type: integer
          minimum: 0
        byPartner:
          type: array
          maxItems: 500
          items:
            $ref: "#/components/schemas/ActivityPartnerCount"
    ActivityWindow:
      type: object
      additionalProperties: false
      description: >-
        The period and the window it covers: `start` inclusive and `end` the
        moment of the read.
      required: [period, start, end]
      properties:
        period:
          type: string
          enum: ["30d", "90d", quarter]
        start:
          type: string
          format: date-time
        end:
          type: string
          format: date-time
    ActivityDefinitions:
      type: object
      additionalProperties: false
      description: What each figure of the summary and the window means, the same on every response.
      required: [requestsSent, requestsAnswered, accountsInfluenced, window]
      properties:
        requestsSent:
          type: string
          minLength: 1
          maxLength: 500
        requestsAnswered:
          type: string
          minLength: 1
          maxLength: 500
        accountsInfluenced:
          type: string
          minLength: 1
          maxLength: 500
        window:
          type: string
          minLength: 1
          maxLength: 500
    ActivityMeta:
      type: object
      additionalProperties: false
      description: The list freshness block plus the window, the summary and the definitions.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations, window, summary, definitions]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
          description: The time of the newest event of the page, or null for an empty page.
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        window:
          $ref: "#/components/schemas/ActivityWindow"
        summary:
          $ref: "#/components/schemas/ActivitySummary"
        definitions:
          $ref: "#/components/schemas/ActivityDefinitions"
    ActivityResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ActivityEvent"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ActivityMeta"
    TimelineActor:
      description: >-
        Who or what made an entry: a kind and a display name, never an id. A
        `system` entry is shown as `Co-Signal sync`; an `automation_policy`
        entry also carries the policy version it ran under.
      oneOf:
        - type: object
          additionalProperties: false
          required: [kind, name]
          properties:
            kind:
              type: string
              enum: [person]
            name:
              type: string
              minLength: 1
              maxLength: 200
        - type: object
          additionalProperties: false
          required: [kind, name]
          properties:
            kind:
              type: string
              enum: [app]
            name:
              type: string
              minLength: 1
              maxLength: 200
        - type: object
          additionalProperties: false
          required: [kind, name]
          properties:
            kind:
              type: string
              enum: [integration]
            name:
              type: string
              minLength: 1
              maxLength: 200
        - type: object
          additionalProperties: false
          required: [kind, name, version]
          properties:
            kind:
              type: string
              enum: [automation_policy]
            name:
              type: string
              minLength: 1
              maxLength: 200
            version:
              type: integer
              minimum: 1
        - type: object
          additionalProperties: false
          required: [kind, name]
          properties:
            kind:
              type: string
              enum: [system]
            name:
              type: string
              minLength: 1
              maxLength: 200
    TimelineEntry:
      type: object
      additionalProperties: false
      description: >-
        One entry on an account's timeline. A `note` carries its `body`; every
        other kind carries a fixed `template` key and, where it has them, a few
        `params`, and never a message body or a partner's value.
      required: [id, kind, actor, body, template, params, actionId, occurredAt]
      properties:
        id:
          type: string
          pattern: "^tle_[A-Za-z0-9_-]+$"
        kind:
          type: string
          enum: [note, request_event, draft_event, crm_sync, crm_command, automation_send]
        actor:
          $ref: "#/components/schemas/TimelineActor"
        body:
          type: string
          maxLength: 1000
          nullable: true
          description: The note text for a `note`; null for every other kind.
        template:
          type: string
          nullable: true
          enum:
            - request_sent
            - request_accepted
            - request_declined
            - request_closed
            - draft_created
            - crm_synced
            - crm_sync_partial
            - crm_command_run
            - automation_sent
            - null
          description: The fixed template key of a system entry; null for a `note`.
        params:
          type: object
          nullable: true
          description: >-
            A few facts of a system entry, such as a provider name, a mapping
            version or counts; null when it has none.
          additionalProperties:
            oneOf:
              - type: string
                maxLength: 128
              - type: integer
        actionId:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
          description: The action that recorded this entry.
        occurredAt:
          type: string
          format: date-time
    TimelineListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/TimelineEntry"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    TimelineEntryCreateRequest:
      type: object
      additionalProperties: false
      required: [accountId, body]
      properties:
        accountId:
          type: string
          maxLength: 256
          description: An `acct_` public id from one of your own responses.
        body:
          type: string
          maxLength: 1000
          description: Trimmed, then 1 to 1,000 characters.
    TimelineActionRef:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
    TimelineEntryCreateResponse:
      type: object
      additionalProperties: false
      required: [entry, action]
      properties:
        entry:
          $ref: "#/components/schemas/TimelineEntry"
        action:
          $ref: "#/components/schemas/TimelineActionRef"
    RequestCreateRequest:
      type: object
      additionalProperties: false
      required: [accountId, partnershipId, type, message]
      properties:
        accountId:
          type: string
          maxLength: 256
          description: An `acct_` public id from one of your own responses.
        partnershipId:
          type: string
          maxLength: 256
          description: A `ptsh_` partnership public id from `GET /partners`.
        partnerContactId:
          type: string
          maxLength: 256
          description: >-
            A `cont_` public id of one of the partner's contacts, from the
            account context, when the request is for that person.
        type:
          type: string
          enum: [intro, context]
        message:
          type: string
          maxLength: 2000
          description: Trimmed, then 1 to 2,000 characters. Shown to the person who approves it and to the partner once it is sent.
    DraftCreateRequest:
      type: object
      additionalProperties: false
      required: [accountId, partnershipId, type, message]
      properties:
        accountId:
          type: string
          maxLength: 256
          description: An `acct_` public id from one of your own responses.
        partnershipId:
          type: string
          maxLength: 256
          description: A `ptsh_` partnership public id from `GET /partners`.
        partnerContactId:
          type: string
          maxLength: 256
          description: >-
            A `cont_` public id of one of the partner's contacts, from the
            account context, when the draft is for that person.
        type:
          type: string
          enum: [intro, context]
        message:
          type: string
          maxLength: 2000
          description: Trimmed, then 1 to 2,000 characters.
    RequestRespondRequest:
      type: object
      additionalProperties: false
      description: Send `{}` when there is no note.
      properties:
        replyNote:
          type: string
          maxLength: 1000
          description: Trimmed, then 1 to 1,000 characters. Shown to the sender.
    RequestCloseRequest:
      type: object
      additionalProperties: false
      required: [attribution]
      properties:
        attribution:
          type: string
          enum: [sourced, influenced, none]
          description: How the partner affected the deal.
        closeNote:
          type: string
          maxLength: 280
          description: Trimmed, then 1 to 280 characters.
    PendingActionRef:
      type: object
      additionalProperties: false
      required: [id, status, approvalUrl, expiresAt]
      properties:
        id:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
        status:
          type: string
          enum: [pending_approval]
        approvalUrl:
          type: string
          format: uri
          maxLength: 2048
          description: >-
            The address in Co-Signal where a member of your workspace who can
            see the account approves the item. Opening it needs a signed in
            member.
        expiresAt:
          type: string
          format: date-time
          description: The end of the seven day approval window.
    PendingActionResponse:
      type: object
      additionalProperties: false
      required: [action]
      properties:
        action:
          $ref: "#/components/schemas/PendingActionRef"
    ActionSucceededRef:
      type: object
      additionalProperties: false
      required: [id, status]
      properties:
        id:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
        status:
          type: string
          enum: [succeeded]
    RequestRef:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id:
          type: string
          pattern: "^req_[A-Za-z0-9_-]+$"
    RequestCreatedResponse:
      type: object
      additionalProperties: false
      required: [action, request]
      properties:
        action:
          $ref: "#/components/schemas/ActionSucceededRef"
        request:
          $ref: "#/components/schemas/RequestRef"
    AnsweredRequestRef:
      type: object
      additionalProperties: false
      required: [id, status]
      properties:
        id:
          type: string
          pattern: "^req_[A-Za-z0-9_-]+$"
        status:
          type: string
          enum: [accepted, declined, closed]
    RequestAnsweredResponse:
      type: object
      additionalProperties: false
      required: [action, request]
      properties:
        action:
          $ref: "#/components/schemas/ActionSucceededRef"
        request:
          $ref: "#/components/schemas/AnsweredRequestRef"
    ActionFailure:
      type: object
      additionalProperties: false
      description: One failure of an action, by a closed code. An internal id of the failed item never leaves.
      required: [code]
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 64
    ActionResult:
      type: object
      additionalProperties: false
      description: The resource the action produced.
      required: [type, id]
      properties:
        type:
          type: string
          minLength: 1
          maxLength: 32
          description: The resource type, for example `request`.
        id:
          type: string
          description: The resource's public id in your workspace.
    ActionStatus:
      type: object
      additionalProperties: false
      required: [id, operation, kind, status, createdAt, updatedAt, expiresAt, approvalUrl, counts, failures, result]
      properties:
        id:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
        operation:
          type: string
          maxLength: 64
          description: The operation that started the action, for example `requests_act`.
        kind:
          type: string
          enum:
            - draft
            - request_send
            - request_respond
            - request_close
            - timeline_note
            - crm_destination_sync
            - crm_command
            - crm_destination_mark_stale
            - crm_destination_cleanup
            - export
            - import
            - import_apply
        status:
          type: string
          enum:
            - pending_approval
            - queued
            - running
            - uncertain
            - succeeded
            - partially_succeeded
            - failed
            - refused
            - expired
            - superseded
          description: >-
            `pending_approval` waits for a named person; `succeeded`,
            `partially_succeeded`, `failed`, `refused`, `expired` and
            `superseded` are final. A pending item that was not approved
            within seven days reads `expired`.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: The end of the approval window while the action is `pending_approval`; null otherwise.
        approvalUrl:
          type: string
          format: uri
          maxLength: 2048
          nullable: true
          description: Where a member approves the item while it is `pending_approval`; null otherwise.
        counts:
          type: object
          nullable: true
          description: Exact counts by name, such as `written` and `failed`; null while the action has none.
          additionalProperties:
            type: integer
            minimum: 0
        failures:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/ActionFailure"
        result:
          allOf:
            - $ref: "#/components/schemas/ActionResult"
          nullable: true
          description: The resource the action produced; null until it produced one.
    ActionStatusResponse:
      type: object
      additionalProperties: false
      required: [action]
      properties:
        action:
          $ref: "#/components/schemas/ActionStatus"
    SuggestionFieldProvenance:
      type: object
      additionalProperties: false
      description: Where one field of a licensed row came from and when it was observed.
      required: [provider, observedAt]
      properties:
        provider:
          type: string
          minLength: 1
          maxLength: 200
        observedAt:
          type: string
          format: date-time
    SuggestionProvenance:
      type: object
      additionalProperties: false
      description: >-
        One entry per field a licensed row can show, null for a field the row
        does not show. The source's own id and licence reference are never
        returned.
      required: [companyName, domain, category, sellsWith]
      properties:
        companyName:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SuggestionFieldProvenance"
        domain:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SuggestionFieldProvenance"
        category:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SuggestionFieldProvenance"
        sellsWith:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SuggestionFieldProvenance"
    SuggestionFactor:
      type: object
      additionalProperties: false
      description: One named factor of the ranking and what it contributed to this row's score.
      required: [name, contribution, explanation]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: One of the names in `meta.factors`.
        contribution:
          type: number
        explanation:
          type: string
          maxLength: 1000
          description: A sentence built from your own data only, such as how many of your active partners share the category.
    SuggestionInvite:
      type: object
      additionalProperties: false
      description: >-
        Where to invite this company: the app's Partnerships page with the
        Invite a partner dialog open. The API never sends an invitation.
      required: [href]
      properties:
        href:
          type: string
          maxLength: 2048
    Suggestion:
      type: object
      additionalProperties: false
      description: One suggested company, ranked for your workspace.
      required: [id, origin, name, domain, category, sellsWith, provenance, score, factors, evidence, inviteAvailable, invite]
      properties:
        id:
          type: string
          description: A `dir_` public id minted to your workspace.
        origin:
          type: string
          enum: [opt_in, licensed]
          description: >-
            `opt_in` is a company that chose to join the directory;
            `licensed` is a record from directory data Co-Signal is licensed to
            use.
        name:
          type: string
          maxLength: 500
        domain:
          type: string
          maxLength: 253
          nullable: true
        category:
          type: string
          nullable: true
          enum: [SaaS, Marketing & Advertising, Financial Services, Healthcare, Professional Services, Retail & E-commerce, Manufacturing, Other]
        sellsWith:
          type: array
          maxItems: 50
          items:
            type: string
            maxLength: 500
        provenance:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SuggestionProvenance"
          description: Present for a `licensed` row and null for an `opt_in` row, which published its own facts.
        score:
          type: number
          description: The sum of the factor contributions; the order of the page is highest first.
        factors:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/SuggestionFactor"
        evidence:
          type: string
          maxLength: 1000
          description: The sentence of the leading factor.
        inviteAvailable:
          type: boolean
          description: False for a licensed company whose source does not allow invitations.
        invite:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SuggestionInvite"
          description: Null exactly when `inviteAvailable` is false.
        explanation:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/GreenfieldExplanation"
          description: >-
            Present only on a request that sent `explain=true`: the first five
            rows of the page carry it and every later row carries null.
    SuggestionsCoverage:
      type: object
      additionalProperties: false
      description: >-
        Counted from the same read as the rows. It never states the size of any
        wider network.
      required: [shown, totalListed, optInListed, licensedListed]
      properties:
        shown:
          type: integer
          minimum: 0
          description: The number of rows in `data`.
        totalListed:
          type: integer
          minimum: 0
          description: Every company counted for you by this read, `optInListed` plus `licensedListed`.
        optInListed:
          type: integer
          minimum: 0
        licensedListed:
          type: integer
          minimum: 0
    SuggestionsMeta:
      type: object
      additionalProperties: false
      description: >-
        The list freshness block plus the computed `state`, the ranking's
        `weightVersion`, the `coverage` of the read and what each factor means.
        `state` is `suggestions` when at least one company is shown and
        `no_candidates` otherwise, whatever the cause.
      required: [requestId, schemaVersion, observedAt, computedAt, sourceUpdatedAt, limitations, state, weightVersion, coverage, factors]
      properties:
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          nullable: true
        limitations:
          type: array
          items:
            $ref: "#/components/schemas/ListLimitation"
        state:
          type: string
          enum: [suggestions, no_candidates]
        weightVersion:
          type: string
          minLength: 1
          maxLength: 64
          description: The version of the ranking weights; changing them is a versioned change.
        coverage:
          $ref: "#/components/schemas/SuggestionsCoverage"
        factors:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/GreenfieldFactorDefinition"
    SuggestionsResponse:
      type: object
      additionalProperties: false
      description: One ranked page of at most 25 suggestions. There is no cursor, because the answer is a top list.
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 25
          items:
            $ref: "#/components/schemas/Suggestion"
        meta:
          $ref: "#/components/schemas/SuggestionsMeta"
    LeadOverlapOwnLead:
      type: object
      additionalProperties: false
      description: Your own lead.
      required: [id, companyName, companyDomain, status]
      properties:
        id:
          type: string
          description: A `lead_` public id.
        companyName:
          type: string
          minLength: 1
          maxLength: 500
          description: A blank stored name is served as `Unnamed company`.
        companyDomain:
          type: string
          maxLength: 253
          nullable: true
        status:
          type: string
          maxLength: 255
          nullable: true
          description: The CRM's own status value, unconstrained.
        owner:
          allOf:
            - $ref: "#/components/schemas/LeadOwner"
          description: OMITTED when the lead has no owner. Nothing is withheld on your own lead.
    LeadOverlapPartnerPopulation:
      type: object
      additionalProperties: false
      description: The population your partner shares with you that contains the matched record.
      required: [id, name]
      properties:
        id:
          type: string
          description: A `ppop_` public id, the same id `GET /partner-populations` issues to your workspace.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The name your partner gave the population.
    LeadOverlapPartnerLead:
      type: object
      additionalProperties: false
      description: >-
        Your partner's lead, as your partner shares it with you. The company,
        status and owner are always present. `personName` and `title` are
        OMITTED unless your partner shares person details with you, and
        `personEmail` is OMITTED unless your partner also shares the email.
      required: [resourceType, companyName, companyDomain, status, owner, customFields]
      properties:
        resourceType:
          type: string
          enum: [lead]
        companyName:
          type: string
          minLength: 1
          maxLength: 500
        companyDomain:
          type: string
          maxLength: 253
          nullable: true
        status:
          type: string
          maxLength: 255
          nullable: true
        owner:
          allOf:
            - $ref: "#/components/schemas/LeadOwner"
          nullable: true
          description: The partner's lead owner, or null when the lead has no owner.
        personName:
          type: string
          minLength: 1
          maxLength: 500
          description: OMITTED unless your partner shares person details with you.
        title:
          type: string
          maxLength: 500
          nullable: true
          description: OMITTED unless your partner shares person details with you; null when the source asserted none.
        personEmail:
          type: string
          maxLength: 320
          nullable: true
          description: OMITTED unless your partner shares person details and the email with you; null when the source asserted none.
        customFields:
          type: array
          description: The partner's fields your partner shares with you, each with its provenance.
          items:
            $ref: "#/components/schemas/PartnerCustomField"
    LeadOverlapPartnerAccount:
      type: object
      additionalProperties: false
      description: Your partner's account for the same company, as your partner shares it with you.
      required: [resourceType, name, domain, customFields]
      properties:
        resourceType:
          type: string
          enum: [account]
        name:
          type: string
          minLength: 1
        domain:
          type: string
          maxLength: 253
          nullable: true
        relationship:
          type: string
          maxLength: 64
          description: The partner's relationship to the account. OMITTED when the partner withholds that field group.
        partnerHasOpenOpportunity:
          type: boolean
          description: OMITTED when the partner withholds that field group; false means shared and there is none.
        customFields:
          type: array
          description: The partner's fields your partner shares with you, each with its provenance.
          items:
            $ref: "#/components/schemas/PartnerCustomField"
    LeadOverlapEntry:
      type: object
      additionalProperties: false
      description: One partner record that matches your lead and that the partner shares with you.
      required: [partnershipId, workspace, match, companyDomain, partnerPopulation, partnerRecord, computedAt]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership id.
        workspace:
          $ref: "#/components/schemas/PartnerWorkspace"
        match:
          type: string
          enum: [company_match, same_person]
          description: >-
            `same_person` only when you and the partner both share person
            details with each other and the work emails match; otherwise
            `company_match`.
        companyDomain:
          type: string
          maxLength: 253
          description: The company domain the match was made on.
        partnerPopulation:
          $ref: "#/components/schemas/LeadOverlapPartnerPopulation"
        partnerRecord:
          oneOf:
            - $ref: "#/components/schemas/LeadOverlapPartnerLead"
            - $ref: "#/components/schemas/LeadOverlapPartnerAccount"
          discriminator:
            propertyName: resourceType
            mapping:
              lead: "#/components/schemas/LeadOverlapPartnerLead"
              account: "#/components/schemas/LeadOverlapPartnerAccount"
        computedAt:
          type: string
          format: date-time
          nullable: true
          description: When this match was last computed.
    LeadOverlapSummary:
      type: object
      additionalProperties: false
      required: [lead, overlaps]
      properties:
        lead:
          $ref: "#/components/schemas/LeadOverlapOwnLead"
        overlaps:
          type: array
          description: Empty until a partner shares a matching record with you through the automation channel.
          items:
            $ref: "#/components/schemas/LeadOverlapEntry"
    LeadOverlapListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/LeadOverlapSummary"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    ResourceRef:
      type: object
      additionalProperties: false
      description: A record named by its public id alone.
      required: [id]
      properties:
        id:
          type: string
          description: A public id with its resource prefix (`acct_`, `lead_`, `ws_`).
    AccountOverlapPairPartner:
      type: object
      additionalProperties: false
      description: >-
        The partner side of one account overlap pair: the partnership and the
        partner workspace by public id, and the overlap facts. The partner's
        name is not repeated here; `GET /partners` serves it.
      required: [partnershipId, workspace, tier, score, reason]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership id.
        workspace:
          $ref: "#/components/schemas/ResourceRef"
        tier:
          type: string
          enum: [exact, strong, possible]
        score:
          type: number
          minimum: 0
        reason:
          type: string
          maxLength: 300
        relationship:
          type: string
          maxLength: 64
          description: The partner's relationship to the account. OMITTED when the partner withholds that field group.
        partnerHasOpenOpportunity:
          type: boolean
          description: OMITTED when the partner withholds that field group; false means shared and there is none.
    AccountOverlapPair:
      type: object
      additionalProperties: false
      description: One of your accounts and one partner it overlaps with, the item of an `account_overlaps` change feed.
      required: [account, partner]
      properties:
        account:
          $ref: "#/components/schemas/ResourceRef"
        partner:
          $ref: "#/components/schemas/AccountOverlapPairPartner"
    LeadOverlapPairPartner:
      type: object
      additionalProperties: false
      description: The partner side of one lead overlap pair, with the entries `GET /overlaps/leads` shows for that lead on that partnership.
      required: [partnershipId, workspace, entries]
      properties:
        partnershipId:
          type: string
          description: The `ptsh_` partnership id.
        workspace:
          $ref: "#/components/schemas/ResourceRef"
        entries:
          type: array
          items:
            $ref: "#/components/schemas/LeadOverlapEntry"
    LeadOverlapPair:
      type: object
      additionalProperties: false
      description: One of your leads and one partner it overlaps with, the item of a `lead_overlaps` change feed.
      required: [lead, partner]
      properties:
        lead:
          $ref: "#/components/schemas/ResourceRef"
        partner:
          $ref: "#/components/schemas/LeadOverlapPairPartner"
    ChangeItem:
      type: object
      additionalProperties: false
      description: >-
        One snapshot or change item. A snapshot item is an `upsert` with `id`
        and `data` only. A change item adds `changeId`, `committedAt` and
        `historical`. A `tombstone` never carries `data` and never a reason:
        it looks the same for every cause.
      required: [type, id]
      properties:
        type:
          type: string
          enum: [upsert, tombstone]
        id:
          type: string
          description: The public id of the record (or, for the overlap resources, the `aovl_` or `lovl_` id of the overlap pair).
        changeId:
          type: string
          description: The `chg_` id of the change. OMITTED on a snapshot item, which is current state rather than a change.
        committedAt:
          type: string
          format: date-time
          description: When the change was committed. OMITTED on a snapshot item.
        historical:
          type: boolean
          description: True for an initial backfill entry, which is never a new event. OMITTED on a snapshot item.
        data:
          description: >-
            The resource's row: an `AccountSummary`, `ContactSummary`,
            `OpportunitySummary` or `LeadSummary` list row, or an
            `AccountOverlapPair` or `LeadOverlapPair`. A changed lead carries
            its list row; its chosen custom field values stay on
            `GET /leads/{leadId}`. OMITTED on a tombstone.
          oneOf:
            - $ref: "#/components/schemas/AccountSummary"
            - $ref: "#/components/schemas/ContactSummary"
            - $ref: "#/components/schemas/OpportunitySummary"
            - $ref: "#/components/schemas/LeadSummary"
            - $ref: "#/components/schemas/AccountOverlapPair"
            - $ref: "#/components/schemas/LeadOverlapPair"
    ChangeFeedPage:
      type: object
      additionalProperties: false
      description: >-
        On a snapshot page, `nextCursor` names the next page and `hasMore` is
        true until the last page. On a change page, `nextCursor` is always
        null and `hasMore` true means call again at once with the new
        bookmark.
      required: [nextCursor, hasMore]
      properties:
        nextCursor:
          type: string
          maxLength: 1024
          nullable: true
        hasMore:
          type: boolean
    ChangeFeedResponse:
      type: object
      additionalProperties: false
      description: >-
        A snapshot page that is not the last carries `page.nextCursor` and a
        null `bookmark`; the last snapshot page and every change page carry
        the `bookmark` to keep.
      required: [data, page, bookmark, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ChangeItem"
        page:
          $ref: "#/components/schemas/ChangeFeedPage"
        bookmark:
          type: string
          maxLength: 1024
          nullable: true
          description: Valid for 30 days. Bound to the credential, workspace, resource and query that created it.
        meta:
          $ref: "#/components/schemas/ListMeta"
    ChangeFeedResync:
      type: object
      additionalProperties: false
      required: [action, href]
      properties:
        action:
          type: string
          enum: [snapshot]
        href:
          type: string
          description: The call that starts a new snapshot of the same resource, such as `/api/v1/changes?resource=accounts`, or of the same signal feed, such as `/api/v1/signals/accounts`.
    ChangeFeedResyncError:
      type: object
      additionalProperties: false
      description: The `410` answer of `GET /changes` and of the three signal feeds. Take a new snapshot and replace your copy.
      required: [error, requestId, resync]
      properties:
        error:
          type: string
          enum: [cursor_expired, resync_required]
        requestId:
          type: string
          minLength: 16
          maxLength: 128
        resync:
          $ref: "#/components/schemas/ChangeFeedResync"
    SignalFeedItem:
      type: object
      additionalProperties: false
      description: >-
        One signal, thin on purpose (the type, the time and public ids, never a
        name, a domain, an amount or a message). `id` is the `sig_` id of this
        signal for you, `event` the `sev_` id of the source change, `subject`
        the primary public id of the type, and `data` holds the public ids and
        fixed labels of the type, equal to the `data` of the webhook for the
        same signal. An `overlap.removed` item carries only the overlap id and
        `kind`, and an `attribution.updated` item (own deals feed, admin
        principals only) also names the credited partners and carries the
        deal's value. `historical` is true on every snapshot item and on a baseline
        signal, and false on a live change. Read the current record from the
        API with the id, because the item does not carry it.
      required: [id, event, type, time, subject, data, historical]
      properties:
        id:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        event:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        type:
          type: string
          enum:
            - overlap.created
            - overlap.updated
            - overlap.removed
            - partner_deal_opened
            - partner_deal_closed_won
            - partner_greenfield_closed_won
            - own_deal_opened
            - own_deal_closed_won
            - attribution.updated
        time:
          type: string
          format: date-time
        subject:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        data:
          description: >-
            The public ids and fixed labels of the type, a map of short
            strings equal to the `data` of the webhook for the same signal.
            Only `attribution.updated` differs: its `data` is
            `AttributionEventData`, with the credited partners and the deal
            value. For every other type it is the string map, as described
            above.
          oneOf:
            - type: object
              additionalProperties:
                type: string
                nullable: true
                minLength: 1
                maxLength: 64
            - $ref: "#/components/schemas/AttributionEventData"
        historical:
          type: boolean
    SignalFeedResponse:
      type: object
      additionalProperties: false
      description: >-
        The change feed's own envelope. A snapshot page that is not the last
        carries `page.nextCursor` and a null `bookmark`; the last snapshot page
        and every change page carry the `bookmark` to keep. A page can hold
        fewer items than `limit` while `page.hasMore` is true.
      required: [data, page, bookmark, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/SignalFeedItem"
        page:
          $ref: "#/components/schemas/ChangeFeedPage"
        bookmark:
          type: string
          maxLength: 1024
          nullable: true
          description: Valid for 30 days. Bound to the credential, workspace, feed and `types` list that created it.
        meta:
          $ref: "#/components/schemas/ListMeta"
    AttributionEventData:
      type: object
      additionalProperties: false
      description: >-
        The `data` of an `attribution.updated` webhook or feed item, the only
        `data` that carries partner names and a value; every other event type
        keeps the map of public ids and labels. The event fires after
        attribution is recalculated, when a deal's credit kind, its credited
        partners as you may see them, or its value changes, so a change of the
        deal's value alone fires it. A credit that is removed fires it with
        kind `none` and `partners: []`, while a first sighting of a deal with
        no credit fires nothing. A partner that ends the partnership or stops
        sharing on the API channel drops out of the next event for each deal it
        was credited on, and earlier undelivered events that name it are not
        delivered (they are dropped at dispatch and left out of the feed); this
        shows at the next recalculation (nightly, or when a co-sell request
        closes), not instantly. A partner renaming itself fires nothing,
        because names are not part of what is compared, and the name is the one
        current when the event was made. `value` is the deal's value as your CRM
        stores it, in the currency of your CRM, never converted. It is served
        only to an admin principal that holds `ecosystem:attribution:read` and
        `ecosystem:partners:read`, and it never names a partner that ended the
        partnership or does not share on the API channel.
      required: [opportunity, kind, partners, value]
      properties:
        opportunity:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
          description: The `opp_` id of the deal. Read the deal from the API with it.
        kind:
          type: string
          enum: [sourced, influenced, none]
          description: >-
            The credit the deal holds now. `none` means the credit was removed,
            or every credited partner is one you may not name.
        partners:
          type: array
          maxItems: 50
          description: >-
            The credited partners you may name on this channel, possibly none.
          items:
            $ref: "#/components/schemas/AttributionEventPartner"
        value:
          type: number
          description: >-
            The deal's value as your CRM stores it, in the currency of your CRM,
            never converted. A bare number with no currency code.
      example:
        opportunity: opp_example1
        kind: sourced
        partners:
          - partnership: ptsh_example1
            name: Example Partner Co
        value: 48000
    AttributionEventPartner:
      type: object
      additionalProperties: false
      description: >-
        One partner credited on a deal: its `ptsh_` partnership id and its name
        as of when the event was made. Only a partner the API may name on the
        channel being read appears here.
      required: [partnership, name]
      properties:
        partnership:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^ptsh_[A-Za-z0-9_-]+$"
          description: The `ptsh_` id of the partnership.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: The partner's name, 1 to 200 characters.
    KnowledgeSearchHit:
      type: object
      additionalProperties: false
      description: >-
        One cited document with its best matching section. There is no
        action, tool or command field: an instruction inside source text
        stays quoted content.
      required:
        - documentId
        - title
        - canonicalUrl
        - revisionDate
        - sourceRights
        - category
        - heading
        - snippet
        - snippetOmitted
        - contentTrust
        - quoted
        - stale
        - staleSince
        - score
      properties:
        documentId:
          type: string
        title:
          type: string
        canonicalUrl:
          type: string
          format: uri
          description: An https link on a Co-Signal host.
        revisionDate:
          type: string
          format: date
        sourceRights:
          type: string
          enum: [first_party, authorized_public_learning, licensed_public]
        category:
          type: string
        heading:
          type: string
          nullable: true
        snippet:
          type: string
          nullable: true
          maxLength: 480
          description: Bounded quoted source text, or null for a citation only entry.
        snippetOmitted:
          type: boolean
        contentTrust:
          type: string
          enum: [untrusted_source_text]
        quoted:
          type: boolean
          enum: [true]
        stale:
          type: boolean
          description: True when the revision date is older than the 365 day freshness window.
        staleSince:
          type: string
          format: date
          nullable: true
        score:
          type: number
    KnowledgeStaleDocument:
      type: object
      additionalProperties: false
      required: [documentId, title, canonicalUrl, revisionDate, sourceRights, staleSince]
      properties:
        documentId:
          type: string
        title:
          type: string
        canonicalUrl:
          type: string
          format: uri
        revisionDate:
          type: string
          format: date
        sourceRights:
          type: string
          enum: [first_party, authorized_public_learning, licensed_public]
        staleSince:
          type: string
          format: date
    KnowledgeSearchData:
      type: object
      additionalProperties: false
      required: [query, noResults, results, staleDocuments]
      properties:
        query:
          type: string
          description: The normalized query that was searched.
        noResults:
          type: boolean
        results:
          type: array
          items:
            $ref: "#/components/schemas/KnowledgeSearchHit"
        staleDocuments:
          type: array
          items:
            $ref: "#/components/schemas/KnowledgeStaleDocument"
    KnowledgeSearchMeta:
      type: object
      additionalProperties: false
      required:
        - requestId
        - schemaVersion
        - observedAt
        - computedAt
        - sourceUpdatedAt
        - indexVersion
        - indexGeneratedAt
        - destinationId
        - limitations
      properties:
        requestId:
          type: string
        schemaVersion:
          type: string
          enum: ["2026-09-20"]
        observedAt:
          type: string
          format: date-time
        computedAt:
          type: string
          format: date-time
        sourceUpdatedAt:
          type: string
          format: date-time
          description: The index generation time.
        indexVersion:
          type: string
        indexGeneratedAt:
          type: string
          format: date-time
        destinationId:
          type: string
          nullable: true
        limitations:
          type: array
          description: >-
            Free text states such as `no_matching_documents`,
            `stale_index_documents_present` and
            `source_rights_limited_snippets`, plus the standing notes that the
            corpus is not live tenant data and that retrieved content is
            untrusted source text.
          items:
            type: string
    KnowledgeSearchResponse:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          $ref: "#/components/schemas/KnowledgeSearchData"
        meta:
          $ref: "#/components/schemas/KnowledgeSearchMeta"
    CrmCommandPreviewRequest:
      type: object
      additionalProperties: false
      description: Asks for the preview of a CRM command. Writes nothing.
      required: [provider, mode]
      properties:
        provider:
          type: string
          enum: [salesforce, hubspot]
          description: Which of your connected CRMs the command is about.
        mode:
          type: string
          enum: [preview]
    CrmCommandExecuteRequest:
      type: object
      additionalProperties: false
      description: >-
        Executes the preview whose `payloadHash` this carries, and only that
        one. Needs an `Idempotency-Key`. A hash that is no longer current, or
        is not a hash a preview answered, is `409 preview_stale`.
      required: [provider, payloadHash]
      properties:
        provider:
          type: string
          enum: [salesforce, hubspot]
        payloadHash:
          type: string
          minLength: 1
          maxLength: 128
          description: The `payloadHash` of a preview, for example `sh1:` followed by 64 lowercase hex characters.
    CrmCommandAccountTarget:
      type: object
      additionalProperties: false
      description: A preview record about the account itself.
      required: [type, accountId]
      properties:
        type:
          type: string
          enum: [account]
        accountId:
          type: string
          pattern: "^acct_[A-Za-z0-9_-]+$"
    CrmCommandOverlapTarget:
      type: object
      additionalProperties: false
      description: A preview record about one overlap pair of the account.
      required: [type, overlapId]
      properties:
        type:
          type: string
          enum: [overlap]
        overlapId:
          type: string
          pattern: "^aovl_[A-Za-z0-9_-]+$"
    CrmCommandRecord:
      type: object
      additionalProperties: false
      description: One record the command would write. It names its fields, never their values.
      required: [target, fields]
      properties:
        target:
          oneOf:
            - $ref: "#/components/schemas/CrmCommandAccountTarget"
            - $ref: "#/components/schemas/CrmCommandOverlapTarget"
          discriminator:
            propertyName: type
            mapping:
              account: "#/components/schemas/CrmCommandAccountTarget"
              overlap: "#/components/schemas/CrmCommandOverlapTarget"
        fields:
          type: array
          maxItems: 200
          description: The CRM field API names the command would write for this record, in the order of the approved mapping.
          items:
            type: string
            minLength: 1
            maxLength: 128
    CrmCommandPreview:
      type: object
      additionalProperties: false
      required: [payloadHash, provider, destinationType, mappingVersion, records]
      properties:
        payloadHash:
          type: string
          pattern: "^sh1:[0-9a-f]{64}$"
          description: Send this back to execute exactly this preview.
        provider:
          type: string
          enum: [salesforce, hubspot]
        destinationType:
          type: string
          enum: [account_fields, company_properties, overlap_object]
          description: What the command writes into, which is a Salesforce Account, a HubSpot company or the overlap object.
        mappingVersion:
          type: integer
          minimum: 0
          description: The version of the approved destination mapping the preview was computed under.
        records:
          type: array
          maxItems: 1000
          items:
            $ref: "#/components/schemas/CrmCommandRecord"
    CrmCommandPreviewResponse:
      type: object
      additionalProperties: false
      required: [preview]
      properties:
        preview:
          $ref: "#/components/schemas/CrmCommandPreview"
    CrmCommandActionRef:
      type: object
      additionalProperties: false
      required: [id, status]
      properties:
        id:
          type: string
          pattern: "^act_[A-Za-z0-9_-]+$"
        status:
          type: string
          description: The action's state when the call returned; follow it with `GET /actions/{actionId}`.
          enum:
            - pending_approval
            - queued
            - running
            - uncertain
            - succeeded
            - partially_succeeded
            - failed
            - refused
            - expired
            - superseded
    CrmCommandAcceptedResponse:
      type: object
      additionalProperties: false
      required: [action]
      properties:
        action:
          $ref: "#/components/schemas/CrmCommandActionRef"
    WebhookSubscriptionFilters:
      type: object
      additionalProperties: false
      description: >-
        The partnerships a subscription is narrowed to, as the `ptsh_` ids you
        named. An empty list means every partnership. No other filter exists.
      required: [partnershipIds]
      properties:
        partnershipIds:
          type: array
          maxItems: 100
          items:
            type: string
            minLength: 4
            maxLength: 64
            pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
    WebhookSubscription:
      type: object
      additionalProperties: false
      description: >-
        One webhook subscription. It never carries the signing secret (shown
        once, when it is made), the challenge token, the credential it is bound
        to or the workspace id. `status` is `pending_verification` (waiting for
        the endpoint to answer the handshake), `active`, `verification_failed`
        (the handshake failed three times), `suspended` (the credential that
        made it lapsed or lost a scope its event types need; `statusReason`
        names the public cause) or `disabled` (you disabled it, or the endpoint
        answered `410 Gone`). A subscription is delivered to only while it is
        `active`.
      required:
        - id
        - url
        - eventTypes
        - filters
        - status
        - statusReason
        - createdAt
        - updatedAt
        - verifiedAt
        - signingKeyIds
      properties:
        id:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^whs_[A-Za-z0-9_-]+$"
        url:
          type: string
          minLength: 1
          maxLength: 2048
        eventTypes:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - overlap.created
              - overlap.updated
              - overlap.removed
              - request.status_changed
              - partner_deal_opened
              - partner_deal_closed_won
              - partner_greenfield_closed_won
              - own_deal_opened
              - own_deal_closed_won
              - data_health.changed
              - attribution.updated
        filters:
          $ref: "#/components/schemas/WebhookSubscriptionFilters"
        status:
          type: string
          enum:
            - pending_verification
            - active
            - verification_failed
            - suspended
            - disabled
        statusReason:
          type: string
          minLength: 1
          maxLength: 64
          nullable: true
          description: >-
            The one public reason a subscription is not active: `verification_failed`,
            `disabled_by_owner`, `endpoint_gone` or, for a suspension,
            `workspace_disabled`, `credential_inactive`, `creator_not_admin` or
            `scope_removed`; null otherwise. Treat a value you do not know as a
            reason, not as an error.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        verifiedAt:
          type: string
          format: date-time
          nullable: true
          description: When the endpoint last answered the handshake; null while it has not.
        signingKeyIds:
          type: array
          maxItems: 8
          description: >-
            The `wsk_` ids of the keys that sign this subscription's
            deliveries now, newest first: one, or two for the 24 hours after a
            rotation. A key id names the key a receiver matches to an entry of
            the `X-CoSignal-Signature` header. It is never a secret.
          items:
            type: string
            pattern: "^wsk_[A-Za-z0-9_-]{1,64}$"
    WebhookSubscriptionCreate:
      type: object
      additionalProperties: false
      description: >-
        The body of `POST /webhook-subscriptions`. Any other key is `400
        invalid_request`.
      required: [url, eventTypes]
      properties:
        url:
          type: string
          minLength: 1
          maxLength: 2048
          description: >-
            The endpoint: HTTPS on port 443, no user name or password, a
            public host. Any other form is `400 invalid_request` with no detail.
        eventTypes:
          type: array
          minItems: 1
          maxItems: 11
          items:
            type: string
            enum:
              - overlap.created
              - overlap.updated
              - overlap.removed
              - request.status_changed
              - partner_deal_opened
              - partner_deal_closed_won
              - partner_greenfield_closed_won
              - own_deal_opened
              - own_deal_closed_won
              - data_health.changed
              - attribution.updated
        filters:
          $ref: "#/components/schemas/WebhookSubscriptionFilters"
    WebhookSubscriptionUpdate:
      type: object
      additionalProperties: false
      description: >-
        The body of `PATCH /webhook-subscriptions/{subscriptionId}`: at least
        one of the four fields. Any other key is `400 invalid_request`.
      properties:
        url:
          type: string
          minLength: 1
          maxLength: 2048
        eventTypes:
          type: array
          minItems: 1
          maxItems: 11
          items:
            type: string
            enum:
              - overlap.created
              - overlap.updated
              - overlap.removed
              - request.status_changed
              - partner_deal_opened
              - partner_deal_closed_won
              - partner_greenfield_closed_won
              - own_deal_opened
              - own_deal_closed_won
              - data_health.changed
              - attribution.updated
        filters:
          $ref: "#/components/schemas/WebhookSubscriptionFilters"
        status:
          type: string
          enum: [active, disabled]
          description: >-
            `disabled` stops deliveries. `active` reactivates a disabled,
            suspended or failed subscription, or one still pending
            verification, and starts the handshake again.
    WebhookSubscriptionResponse:
      type: object
      additionalProperties: false
      description: The answer of `GET` and `PATCH /webhook-subscriptions/{subscriptionId}`.
      required: [subscription]
      properties:
        subscription:
          $ref: "#/components/schemas/WebhookSubscription"
    WebhookSecret:
      type: object
      additionalProperties: false
      description: >-
        The answer of `POST /webhook-subscriptions` (`201`) and of `POST
        /webhook-subscriptions/{subscriptionId}/rotate` (`200`): the
        subscription and its signing secret, shown ONCE. `secret` is the
        whole `whsec_` string, the key of every signature; it is absent on a
        replay of the same `Idempotency-Key` and body, and no other route
        returns it.
      required: [subscription]
      properties:
        subscription:
          $ref: "#/components/schemas/WebhookSubscription"
        secret:
          type: string
          pattern: "^whsec_[A-Za-z0-9_-]{16,128}$"
    WebhookSubscriptionListResponse:
      type: object
      additionalProperties: false
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookSubscription"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    WebhookDelivery:
      type: object
      additionalProperties: false
      description: >-
        One delivery of one event to one subscription: ids, the type, a closed
        status word, counts, times, the status code of the last attempt and
        its public outcome. It never carries a response body, a header, a URL,
        an internal failure reason, the subject references of the event or the
        note a discard stored. `id` is the `dlv_` id your endpoint received as
        `X-CoSignal-Delivery-Id`; it stays the same across every retry and every
        replay. `status` is `pending` (waiting for an attempt), `retry_scheduled`
        (a retryable answer; the next attempt is at `nextAttemptAt`),
        `delivered`, `dead_letter` (every retry was used), `requires_correction`
        (your endpoint answered with a status that is not retried), `endpoint_disabled`
        (your endpoint answered `410 Gone` and the subscription was disabled),
        `dropped` (the data could no longer be sent to you) or `discarded` (you
        discarded the dead letter). `attempts` counts the current run, which a
        replay restarts at 0. `lastAttemptAt`, `lastStatusCode` and `outcome`
        describe the last attempt MADE: after a replay they keep the previous
        run's values until the first attempt of the new run lands.
      required:
        - id
        - signal
        - event
        - type
        - status
        - attempts
        - createdAt
        - firstAttemptAt
        - lastAttemptAt
        - nextAttemptAt
        - deliveredAt
        - deadLetteredAt
        - lastStatusCode
        - outcome
        - replayCount
      properties:
        id:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        signal:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
          description: The `sig_` id of the signal this delivery carries (the CloudEvents `id`).
        event:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
          description: The `sev_` id of the source change (the `cosignaleventid`).
        type:
          type: string
          enum:
            - overlap.created
            - overlap.updated
            - overlap.removed
            - request.status_changed
            - partner_deal_opened
            - partner_deal_closed_won
            - partner_greenfield_closed_won
            - own_deal_opened
            - own_deal_closed_won
            - data_health.changed
            - attribution.updated
            - webhook.test
        status:
          type: string
          enum:
            - pending
            - delivered
            - retry_scheduled
            - dead_letter
            - requires_correction
            - endpoint_disabled
            - dropped
            - discarded
        attempts:
          type: integer
          minimum: 0
        createdAt:
          type: string
          format: date-time
        firstAttemptAt:
          type: string
          format: date-time
          nullable: true
        lastAttemptAt:
          type: string
          format: date-time
          nullable: true
        nextAttemptAt:
          type: string
          format: date-time
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        deadLetteredAt:
          type: string
          format: date-time
          nullable: true
        lastStatusCode:
          type: integer
          minimum: 100
          maximum: 999
          nullable: true
          description: The status code of the last attempt made; the previous run's value until the first attempt of a replay lands.
        outcome:
          type: string
          nullable: true
          enum: [delivered, http_status, timeout, tls_error, unreachable, null]
          description: The public outcome of the last attempt made, never an internal reason; the previous run's value until the first attempt of a replay lands.
        replayCount:
          type: integer
          minimum: 0
          description: How many times the delivery was replayed; 0 for a delivery that never was.
    WebhookDeliveryResponse:
      type: object
      additionalProperties: false
      description: >-
        The answer of `GET /deliveries/{deliveryId}` (`200`), of `POST
        /deliveries/{deliveryId}/replay` (`202`) and of `POST
        /deliveries/{deliveryId}/discard` (`200`): the delivery as it reads now.
      required: [delivery]
      properties:
        delivery:
          $ref: "#/components/schemas/WebhookDelivery"
    WebhookDeliveryList:
      type: object
      additionalProperties: false
      description: One page of a subscription's delivery log, newest first.
      required: [data, page, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDelivery"
        page:
          $ref: "#/components/schemas/ListPage"
        meta:
          $ref: "#/components/schemas/ListMeta"
    WebhookDeliveryDiscard:
      type: object
      additionalProperties: false
      description: >-
        The body of `POST /deliveries/{deliveryId}/discard`. Any other key is
        `400 invalid_request`.
      required: [reason]
      properties:
        reason:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            Your own note, 1 to 200 characters (counted as characters; the text
            is trimmed first, so only white space is refused). It is stored with
            the delivery for you and is never shown back.
    WebhookTestOutcomeStatus:
      type: object
      additionalProperties: false
      description: The endpoint answered, but not with 2xx. `status` is the code it answered.
      required: [outcome, status]
      properties:
        outcome:
          type: string
          enum: [http_status]
        status:
          type: integer
          minimum: 100
          maximum: 999
    WebhookTestOutcomeFailure:
      type: object
      additionalProperties: false
      description: >-
        The endpoint could not be reached: `timeout` (no answer in time),
        `tls_error` (the certificate or handshake was refused) or `unreachable`
        (the name did not resolve, the connection failed or the address is one
        Co-Signal never sends to). Nothing more is said about why.
      required: [outcome]
      properties:
        outcome:
          type: string
          enum: [timeout, tls_error, unreachable]
    WebhookTestDelivered:
      type: object
      additionalProperties: false
      description: The endpoint answered the test event with 2xx.
      required: [delivered, outcome]
      properties:
        delivered:
          type: boolean
          enum: [true]
        outcome:
          type: string
          enum: [delivered]
    WebhookTestNotDelivered:
      type: object
      additionalProperties: false
      description: The endpoint did not acknowledge the test event; `outcome` is the public outcome of the one attempt.
      required: [delivered, outcome]
      properties:
        delivered:
          type: boolean
          enum: [false]
        outcome:
          oneOf:
            - $ref: "#/components/schemas/WebhookTestOutcomeStatus"
            - $ref: "#/components/schemas/WebhookTestOutcomeFailure"
    WebhookTestResult:
      description: >-
        What a test call answers: whether the endpoint acknowledged the event
        (`delivered`) and the public outcome of the one attempt. Never a
        reason, a header or a body.
      oneOf:
        - $ref: "#/components/schemas/WebhookTestDelivered"
        - $ref: "#/components/schemas/WebhookTestNotDelivered"
      discriminator:
        propertyName: delivered
        mapping:
          "true": "#/components/schemas/WebhookTestDelivered"
          "false": "#/components/schemas/WebhookTestNotDelivered"
    WebhookEventEnvelope:
      type: object
      additionalProperties: false
      description: >-
        The JSON body of one delivery, a CloudEvents 1.0 object that is thin on
        purpose: the type, the time and public ids, never a name, a domain, an
        amount or a message. It is POSTed with `Content-Type: application/json`
        and the headers `X-CoSignal-Signature`, `X-CoSignal-Timestamp` and
        `X-CoSignal-Delivery-Id`. `id` is the `sig_` id of this event for you
        (one source change touching several of your rows is one event for each
        row), `cosignaleventid` is the `sev_` id of the source change, and the
        delivery id is separate again, so a retry of one delivery keeps its
        delivery id. `time` is when the change was committed. `subject` is the
        primary public id, and `data` holds only the public ids and labels of
        the type: `overlap` (`aovl_` or `lovl_`), `partnership`, `account` and
        `kind` (`account` or `lead`) for `overlap.created` and
        `overlap.updated`; only `overlap` and `kind` for `overlap.removed`;
        `request`, `partnership`, `account` and `status` (`requested`,
        `accepted`, `declined` or `closed`) for `request.status_changed`;
        `overlap`, `partnership` and `account` for `partner_deal_opened` and
        `partner_deal_closed_won`; `partnership` and `candidate` for
        `partner_greenfield_closed_won`; `opportunity` and `account` for the
        own deal types; `workspace`, `source` (`salesforce` or `hubspot`) and
        `state` (`fresh`, `degraded` or `stale`) for `data_health.changed`;
        `opportunity`, `kind` (`sourced`, `influenced` or `none`), `partners` and
        `value` for `attribution.updated`, the one type whose `data` holds
        partner names and a value (see `AttributionEventData`); and
        `subscription` for `webhook.test`. Read the current record from the
        API with the id, because the event does not carry it.
      required: [specversion, id, source, type, time, subject, datacontenttype, cosignaleventid, data]
      properties:
        specversion:
          type: string
          enum: ["1.0"]
        id:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        source:
          type: string
          minLength: 1
          maxLength: 2048
          description: The API base URL.
        type:
          type: string
          enum:
            - overlap.created
            - overlap.updated
            - overlap.removed
            - request.status_changed
            - partner_deal_opened
            - partner_deal_closed_won
            - partner_greenfield_closed_won
            - own_deal_opened
            - own_deal_closed_won
            - data_health.changed
            - attribution.updated
            - webhook.test
        time:
          type: string
          format: date-time
        subject:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        datacontenttype:
          type: string
          enum: [application/json]
        cosignaleventid:
          type: string
          minLength: 4
          maxLength: 64
          pattern: "^[a-z]+_[A-Za-z0-9_-]+$"
        data:
          description: >-
            The public ids and fixed labels of the type, a map of short
            strings. Only `attribution.updated` differs: its `data` is
            `AttributionEventData`, with the credited partners and the deal
            value. For every other type it is the string map, as described
            above.
          oneOf:
            - type: object
              additionalProperties:
                type: string
                nullable: true
                minLength: 1
                maxLength: 64
            - $ref: "#/components/schemas/AttributionEventData"
