{
  "openapi": "3.1.0",
  "info": {
    "title": "TeamPredict API",
    "version": "1.0.0",
    "summary": "Employee retention early-warning signals over HTTPS.",
    "description": "TeamPredict watches the public LinkedIn profile signals that tend to precede a\nresignation and turns them into a risk level per tracked employee. This API exposes\neverything the dashboard does: read your roster and its risk changes, add or untrack\npeople, track competitor companies as poaching opportunities, and register signed\nwebhooks.\n\n**Authentication.** Every request needs an API key as a bearer token. Admins create\nkeys in the dashboard under Settings -> Developers. A key is scoped `read` or\n`read`+`write` at creation and cannot be widened afterwards.\n\n**Billing.** Each tracked person is one seat at $5 per month, prorated. Writing\nthrough the API bills exactly like clicking in the dashboard; there is no separate\nAPI charge. Endpoints that can create a seat are marked `x-affects-billing`.\n\n**Free trial.** 30 days, up to 100 tracked employees.\n\n**Conventions.** JSON in, JSON out. Timestamps are ISO 8601 in UTC. Responses may gain\nnew fields without a version bump, so parse leniently. Rate limit is 120 requests per\nminute per key; over it the API answers 429 with a `Retry-After` header in seconds.",
    "termsOfService": "https://www.teampredict.ai/terms",
    "contact": {
      "name": "TeamPredict support",
      "url": "https://www.teampredict.ai/developers/api",
      "email": "support@teampredict.ai"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://www.teampredict.ai/terms"
    }
  },
  "externalDocs": {
    "description": "Human-readable REST API reference",
    "url": "https://www.teampredict.ai/developers/api"
  },
  "servers": [
    {
      "url": "https://app.teampredict.ai/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Key",
      "description": "What this API key reaches and may do."
    },
    {
      "name": "Company",
      "description": "The company profile, its headcounts and its bill."
    },
    {
      "name": "People",
      "description": "The roster: who is tracked, and their risk changes."
    },
    {
      "name": "Risk changes",
      "description": "Detected profile changes, scored."
    },
    {
      "name": "Competitors",
      "description": "Competitor workspaces, read through the poaching lens."
    },
    {
      "name": "Webhooks",
      "description": "Signed event delivery to your own endpoints."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Key"
        ],
        "operationId": "getMe",
        "summary": "Check a key",
        "description": "Which company this key reaches, the competitor workspaces reachable through it, and what the key is allowed to do. The right first call in any integration: it confirms the key works and whether it can write.",
        "responses": {
          "200": {
            "description": "Key introspection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organization": {
      "get": {
        "tags": [
          "Company"
        ],
        "operationId": "getOrganization",
        "summary": "Retrieve a company",
        "description": "The company profile and its headcounts. Safe fields only - billing state lives on /billing.",
        "parameters": [
          {
            "name": "organizationId",
            "in": "query",
            "required": false,
            "description": "A competitor workspace id to act inside instead of your own company. Omit for your own company.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Company"
        ],
        "operationId": "updateOrganization",
        "summary": "Update a company",
        "description": "Edit the company profile and the score at which alert emails fire. The LinkedIn page is deliberately not editable here: it decides which company the daily scan watches.",
        "x-required-scope": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationUpdate"
              },
              "example": {
                "alertRiskThreshold": 0.75
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/billing": {
      "get": {
        "tags": [
          "Company"
        ],
        "operationId": "getBilling",
        "summary": "Retrieve seats and cost",
        "description": "Billed seats across the company and its competitor workspaces, the plan and per-seat rate, an estimated recurring total, and what is left under both seat ceilings. Read this before a large batch. Totals are estimates; the Stripe invoice is the source of truth.",
        "responses": {
          "200": {
            "description": "Seats, plan and allowances.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Billing"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/employees": {
      "get": {
        "tags": [
          "People"
        ],
        "operationId": "listEmployees",
        "summary": "List people",
        "description": "The roster, newest first, each person carrying their latest visible change and risk level.",
        "parameters": [
          {
            "name": "organizationId",
            "in": "query",
            "required": false,
            "description": "A competitor workspace id to act inside instead of your own company. Omit for your own company.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Search name, title and location.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tracked",
            "in": "query",
            "required": false,
            "description": "Filter to tracked (billed) or untracked people.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Rows per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of people.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "People"
        ],
        "operationId": "createEmployee",
        "summary": "Add a person",
        "description": "Start tracking someone by their public LinkedIn profile URL. The row is created immediately and the profile fetch finishes in the background, so the response comes back with status \"importing\" and a placeholder name. A tracked add is one billed seat; send tracked: false to stage someone with no seat and no charge.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmployeeCreate"
              },
              "example": {
                "linkedinUrl": "https://www.linkedin.com/in/jordanlee",
                "tracked": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The person, still importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Employee"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "That LinkedIn profile is already on the roster.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This profile is already in the company.",
                  "code": "EMPLOYEE_EXISTS"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Too many concurrent adds for this company. Retry after the delay in Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Too many additions at once. Try again shortly.",
                  "code": "SEAT_RESERVATION_BUSY"
                }
              }
            }
          }
        }
      }
    },
    "/employees/bulk": {
      "post": {
        "tags": [
          "People"
        ],
        "operationId": "createEmployeesBulk",
        "summary": "Add people in bulk",
        "description": "Up to 50 people in one call. Partial success is the normal outcome: everything that can be added is, and the rest comes back in `skipped` with a reason. 201 when the whole list landed, 207 when some of it did not.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmployeeBulkCreate"
              },
              "example": {
                "linkedinUrls": [
                  "https://www.linkedin.com/in/jordanlee",
                  "https://www.linkedin.com/in/samirpatel"
                ],
                "tracked": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Every URL was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeeBulkResult"
                }
              }
            }
          },
          "207": {
            "description": "Some URLs were added and some were skipped; read `skipped`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployeeBulkResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/employees/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EmployeeId"
        }
      ],
      "get": {
        "tags": [
          "People"
        ],
        "operationId": "getEmployee",
        "summary": "Retrieve a person",
        "description": "One person with their latest visible change and risk level. Works for your own roster and for anyone inside a competitor workspace this key reaches.",
        "responses": {
          "200": {
            "description": "The person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Employee"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "People"
        ],
        "operationId": "updateEmployee",
        "summary": "Update a person",
        "description": "Turn tracking on or off, or archive and restore. Tracking is the billing switch: off releases the seat while keeping the person and their history. Names and LinkedIn URLs mirror LinkedIn and are not editable.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmployeeUpdate"
              },
              "example": {
                "tracked": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Employee"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "People"
        ],
        "operationId": "deleteEmployee",
        "summary": "Remove a person",
        "description": "Archives the person and releases their seat, keeping their change history. To erase the record for good, archive first and then call again with ?permanent=true - two deliberate steps, so one stray call can never destroy history.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "parameters": [
          {
            "name": "permanent",
            "in": "query",
            "required": false,
            "description": "Erase an already-archived person and their history. Cannot be undone.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The person was archived or erased.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedEmployee"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/employees/{id}/changes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EmployeeId"
        }
      ],
      "get": {
        "tags": [
          "People"
        ],
        "operationId": "listEmployeeChanges",
        "summary": "List a person's changes",
        "description": "That person's detected profile changes with risk scores, newest first, windowed to what your company is allowed to see.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Rows per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/changes": {
      "get": {
        "tags": [
          "Risk changes"
        ],
        "operationId": "listChanges",
        "summary": "List risk changes",
        "description": "The company-wide feed of detected profile changes with risk scores, newest first. The endpoint most integrations poll: filter by `since` and `minRiskScore` and you get exactly the signals worth acting on.",
        "parameters": [
          {
            "name": "organizationId",
            "in": "query",
            "required": false,
            "description": "A competitor workspace id to act inside instead of your own company. Omit for your own company.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1
            }
          },
          {
            "name": "minRiskScore",
            "in": "query",
            "required": false,
            "description": "Only changes at or above this score. Departures are always included.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Only changes detected at or after this time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "changeType",
            "in": "query",
            "required": false,
            "description": "Restrict to one category of change.",
            "schema": {
              "type": "string",
              "enum": [
                "profile_update",
                "headline_change",
                "title_change",
                "open_to_work",
                "new_skills",
                "location_change",
                "photo_or_summary",
                "edit_activity",
                "experience_change",
                "employee_departed"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Rows per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/competitors": {
      "get": {
        "tags": [
          "Competitors"
        ],
        "operationId": "listCompetitors",
        "summary": "List competitors",
        "description": "The competitor workspaces your company tracks, each with its tracked-people count. Use an id as `organizationId` on /employees and /changes to read that competitor through the poaching lens, where a high score means \"may be open to a move\".",
        "responses": {
          "200": {
            "description": "Every tracked competitor.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CompetitorSummary"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Competitors"
        ],
        "operationId": "createCompetitor",
        "summary": "Track a competitor",
        "description": "Start tracking a competitor from their LinkedIn company page. The workspace is created immediately; its roster imports in the background. Creating the workspace costs nothing, but the people it finds are tracked profiles on your subscription - pass roleFilters to keep the import narrow.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompetitorCreate"
              },
              "example": {
                "companyLinkedInUrl": "https://www.linkedin.com/company/contoso/",
                "roleFilters": [
                  "engineering",
                  "product"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The workspace, roster still importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompetitorCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The page does not belong to the brand you named. Resend with acknowledgeIdentityMismatch: true to track it anyway.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "That LinkedIn page resolves to a different company than the name you gave.",
                  "code": "COMPETITOR_IDENTITY_MISMATCH"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/competitors/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompetitorId"
        }
      ],
      "get": {
        "tags": [
          "Competitors"
        ],
        "operationId": "getCompetitor",
        "summary": "Retrieve a competitor",
        "description": "One competitor workspace: its role filters, whether it is paused, and how many of its people are tracked.",
        "responses": {
          "200": {
            "description": "The competitor workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Competitors"
        ],
        "operationId": "updateCompetitor",
        "summary": "Pause or retarget a competitor",
        "description": "paused: true is the reversible way to stop paying for a competitor - everyone in the workspace is untracked, so the scan stops and the seats come off your subscription while the people and history stay. roleFilters changes who the import targets and re-runs it.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompetitorUpdate"
              },
              "example": {
                "paused": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Competitors"
        ],
        "operationId": "deleteCompetitor",
        "summary": "Delete a competitor",
        "description": "Erases the workspace, its people and their change history, then releases the seats. This is not \"stop tracking\" - that is PATCH with paused: true, which keeps the history. Deleting cannot be undone.",
        "x-required-scope": "write",
        "x-affects-billing": true,
        "responses": {
          "200": {
            "description": "The workspace was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedCompetitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookEndpoints",
        "summary": "List webhook endpoints",
        "description": "Your registered receivers and their delivery health. Signing secrets are never returned after creation.",
        "responses": {
          "200": {
            "description": "Every registered endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookEndpoint"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "createWebhookEndpoint",
        "summary": "Create a webhook endpoint",
        "description": "Register an HTTPS receiver for employee.warning and poaching.opportunity events. The signing secret is returned once, here - every delivery is HMAC-SHA256 signed with it. Payload shapes and the retry schedule are documented at https://www.teampredict.ai/developers/webhooks.",
        "x-required-scope": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointCreate"
              },
              "example": {
                "url": "https://hooks.example.com/teampredict",
                "description": "Retention alerts into Slack",
                "employeeWarningSensitivity": 4,
                "poachingOpportunities": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The endpoint, including the signing secret (shown once).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointWithSecret"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookEndpointId"
        }
      ],
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "updateWebhookEndpoint",
        "summary": "Update a webhook endpoint",
        "description": "Change the URL, the event switches or their sensitivity, or flip the endpoint off without deleting it. Re-enabling clears the failure counter, so an endpoint auto-disabled after repeated failures gets a clean slate.",
        "x-required-scope": "write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointUpdate"
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete a webhook endpoint",
        "description": "Removes the endpoint and its delivery history. To stop deliveries but keep the record, PATCH { enabled: false } instead.",
        "x-required-scope": "write",
        "responses": {
          "200": {
            "description": "The endpoint was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedWebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "employee.warning": {
      "post": {
        "operationId": "onEmployeeWarning",
        "summary": "employee.warning",
        "description": "One of your own tracked employees showed a signal at or above this endpoint's sensitivity. Read through the retention lens: they may be preparing to leave.",
        "security": [],
        "parameters": [
          {
            "name": "X-TeamPredict-Event",
            "in": "header",
            "required": true,
            "description": "The event type, repeated from the body.",
            "schema": {
              "type": "string",
              "enum": [
                "employee.warning"
              ]
            }
          },
          {
            "name": "X-TeamPredict-Delivery",
            "in": "header",
            "required": true,
            "description": "Unique id for this delivery attempt. Use it to dedupe retries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TeamPredict-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`, an HMAC-SHA256 of `<t>.<raw body>` keyed with the endpoint secret. Verify before trusting the payload.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx marks the delivery successful. Non-2xx is retried with backoff; repeated failures disable the endpoint."
          }
        }
      }
    },
    "poaching.opportunity": {
      "post": {
        "operationId": "onPoachingOpportunity",
        "summary": "poaching.opportunity",
        "description": "Someone inside a tracked competitor workspace showed the same kind of signal. Read through the poaching lens: they may be open to a move.",
        "security": [],
        "parameters": [
          {
            "name": "X-TeamPredict-Event",
            "in": "header",
            "required": true,
            "description": "The event type, repeated from the body.",
            "schema": {
              "type": "string",
              "enum": [
                "poaching.opportunity"
              ]
            }
          },
          {
            "name": "X-TeamPredict-Delivery",
            "in": "header",
            "required": true,
            "description": "Unique id for this delivery attempt. Use it to dedupe retries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TeamPredict-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`, an HMAC-SHA256 of `<t>.<raw body>` keyed with the endpoint secret. Verify before trusting the payload.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx marks the delivery successful. Non-2xx is retried with backoff; repeated failures disable the endpoint."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key created in the dashboard under Settings -> Developers, sent as `Authorization: Bearer tp_...`. Keys are per company and are shown once at creation. See https://www.teampredict.ai/developers/api#authentication."
      }
    },
    "parameters": {
      "EmployeeId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The person's TeamPredict id.",
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 1
        }
      },
      "CompetitorId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The competitor workspace's organization id.",
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 1
        }
      },
      "WebhookEndpointId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The webhook endpoint id, e.g. `whe_2c9f1a`.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A parameter is missing or the wrong type. The message says which.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "linkedinUrl must be a public LinkedIn profile URL.",
              "code": "INVALID_BODY"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, unknown or revoked API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Invalid or missing API key. Send it as: Authorization: Bearer tp_..."
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "A read-only key called a write endpoint.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "This API key is read-only. Create a read/write key to make changes.",
              "code": "INSUFFICIENT_SCOPE"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Blocked on billing: no active subscription, the free-trial seat cap, or a declined seat charge.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Your free trial can track up to 100 people (0 left). Start a paid plan to track more.",
              "code": "TRIAL_LIMIT_REACHED"
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist, or is outside this key's reach.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Organization not found."
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over 120 requests a minute, or the daily seat allowance is spent. `Retry-After` says how long to wait; `code` tells the two apart.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Too many requests. Please try again shortly."
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every failure answers with this shape: a human-readable `error`, plus a machine `code` on the failures worth branching on.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A sentence a person can act on."
          },
          "code": {
            "type": "string",
            "enum": [
              "INVALID_BODY",
              "SUBSCRIPTION_INACTIVE",
              "TRIAL_LIMIT_REACHED",
              "SEAT_CHARGE_FAILED",
              "INSUFFICIENT_SCOPE",
              "EMPLOYEE_EXISTS",
              "COMPETITOR_IDENTITY_MISMATCH",
              "SEAT_QUOTA_EXCEEDED",
              "ROW_QUOTA_EXCEEDED",
              "SEAT_RESERVATION_BUSY"
            ],
            "description": "Stable machine code. Absent on plain 401/404 responses."
          }
        },
        "examples": [
          {
            "error": "Your free trial can track up to 100 people (0 left). Start a paid plan to track more.",
            "code": "TRIAL_LIMIT_REACHED"
          }
        ]
      },
      "OrganizationRef": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "organization",
          "competitors",
          "scopes"
        ],
        "properties": {
          "organization": {
            "$ref": "#/components/schemas/OrganizationRef"
          },
          "competitors": {
            "type": "array",
            "description": "Competitor workspaces this key can also reach.",
            "items": {
              "$ref": "#/components/schemas/OrganizationRef"
            }
          },
          "scopes": {
            "type": "array",
            "description": "What this key may do.",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "write"
              ]
            }
          }
        }
      },
      "Organization": {
        "type": "object",
        "required": [
          "id",
          "name",
          "kind",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "linkedinId": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "primary",
              "competitor"
            ],
            "description": "Your own company, or a competitor workspace."
          },
          "alertRiskThreshold": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "0.5 alerts at Watch and above; 0.75 alerts on High only."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "employeeCount": {
            "type": "integer"
          },
          "trackedEmployeeCount": {
            "type": "integer",
            "description": "The billed seats inside this organization."
          }
        }
      },
      "OrganizationUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "companyInfo": {
            "type": [
              "string",
              "null"
            ],
            "description": "One-paragraph description used as AI risk context."
          },
          "alertRiskThreshold": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Values between 0.5 and 0.75 snap to one of the two."
          },
          "organizationId": {
            "type": "integer",
            "description": "A competitor workspace to edit instead of your own company."
          }
        }
      },
      "Billing": {
        "type": "object",
        "required": [
          "organizationId",
          "seats",
          "plan",
          "estimate",
          "trial",
          "seatSettlement"
        ],
        "properties": {
          "organizationId": {
            "type": "integer"
          },
          "billingOrganizationId": {
            "type": "integer",
            "description": "The organization whose subscription pays for these seats."
          },
          "seats": {
            "type": "object",
            "properties": {
              "billable": {
                "type": "integer",
                "description": "Seats billed across every workspace."
              },
              "inThisOrganization": {
                "type": "integer"
              },
              "organizationIds": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              }
            }
          },
          "plan": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "examples": [
                  "active",
                  "trialing",
                  "past_due",
                  "canceled"
                ]
              },
              "interval": {
                "type": "string",
                "enum": [
                  "month",
                  "year"
                ]
              },
              "perSeatUsd": {
                "type": "number"
              },
              "quantity": {
                "type": "integer"
              },
              "currentPeriodEnd": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "cancelAtPeriodEnd": {
                "type": "boolean"
              }
            }
          },
          "estimate": {
            "type": "object",
            "description": "Estimated from tracked seats and list price. Stripe's invoice wins.",
            "properties": {
              "grossUsd": {
                "type": "number"
              },
              "discountUsd": {
                "type": "number"
              },
              "netUsd": {
                "type": "number"
              },
              "interval": {
                "type": "string",
                "enum": [
                  "month",
                  "year"
                ]
              }
            }
          },
          "trial": {
            "type": "object",
            "properties": {
              "onFreeTrial": {
                "type": "boolean"
              },
              "endsAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "seatLimit": {
                "type": "integer"
              },
              "seatsLeft": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "seatSettlement": {
            "type": "object",
            "description": "Additions settle in batches of 10 seats or $50, whichever lands first, so unbilled additions never run far ahead of the card.",
            "properties": {
              "threshold": {
                "type": "integer"
              },
              "maxUnbilledUsd": {
                "type": "number"
              },
              "unbilledSeats": {
                "type": "integer"
              },
              "estimatedUnbilledUsd": {
                "type": "number"
              },
              "seatsUntilNextCharge": {
                "type": "integer"
              },
              "trigger": {
                "type": "string",
                "enum": [
                  "seats",
                  "amount"
                ]
              },
              "chargeFailed": {
                "type": "boolean",
                "description": "True blocks every further add until a payment clears."
              },
              "chargeFailedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "apiSeatAllowance": {
            "type": "object",
            "description": "The rolling 24-hour ceiling on tracked people added through the API.",
            "properties": {
              "perDay": {
                "type": "integer"
              },
              "usedLast24h": {
                "type": "integer"
              },
              "left": {
                "type": "integer"
              }
            }
          }
        }
      },
      "RiskLevel": {
        "type": "string",
        "enum": [
          "high",
          "watch",
          "low"
        ],
        "description": "The 0-1 score in dashboard tiers: high at 0.75+, watch at 0.50-0.74, low below. A departure carries no numeric score and reads as high."
      },
      "LatestChange": {
        "type": [
          "object",
          "null"
        ],
        "description": "The most recent change visible to your company, or null if there is none.",
        "properties": {
          "riskScore": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "riskLevel": {
            "$ref": "#/components/schemas/RiskLevel"
          },
          "changeType": {
            "type": "string",
            "enum": [
              "profile_update",
              "headline_change",
              "title_change",
              "open_to_work",
              "new_skills",
              "location_change",
              "photo_or_summary",
              "edit_activity",
              "experience_change",
              "employee_departed"
            ]
          },
          "detectedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Employee": {
        "type": "object",
        "required": [
          "id",
          "organizationId",
          "name",
          "linkedinUrl",
          "tracked",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "organizationId": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "department": {
            "type": [
              "string",
              "null"
            ]
          },
          "linkedinUrl": {
            "type": "string",
            "format": "uri"
          },
          "profileImageUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "tracked": {
            "type": "boolean",
            "description": "True means scanned daily and billed as one seat."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "importing",
              "archived"
            ],
            "description": "`importing` while the first profile fetch is still running."
          },
          "trackingStartedAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "latestChange": {
            "$ref": "#/components/schemas/LatestChange"
          }
        }
      },
      "EmployeeCreate": {
        "type": "object",
        "required": [
          "linkedinUrl"
        ],
        "properties": {
          "linkedinUrl": {
            "type": "string",
            "format": "uri",
            "description": "The public profile URL, e.g. https://www.linkedin.com/in/jordanlee. The internal /in/ACoAA... form is rejected."
          },
          "tracked": {
            "type": "boolean",
            "default": true,
            "description": "False adds them to the roster with no seat and no charge."
          },
          "name": {
            "type": "string",
            "description": "Display name to use until LinkedIn answers."
          },
          "organizationId": {
            "type": "integer",
            "description": "Add into a competitor workspace instead of your own company."
          }
        }
      },
      "EmployeeUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "tracked": {
            "type": "boolean",
            "description": "True starts scanning them and takes a seat; false stops both."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ],
            "description": "Archiving always untracks. Restoring sets tracked to false unless you also send tracked: true."
          }
        }
      },
      "EmployeeBulkCreate": {
        "type": "object",
        "required": [
          "linkedinUrls"
        ],
        "properties": {
          "linkedinUrls": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "tracked": {
            "type": "boolean",
            "default": true,
            "description": "Applies to the whole batch."
          },
          "organizationId": {
            "type": "integer"
          }
        }
      },
      "EmployeeBulkResult": {
        "type": "object",
        "required": [
          "organizationId",
          "addedCount",
          "added",
          "skippedCount",
          "skipped"
        ],
        "properties": {
          "organizationId": {
            "type": "integer"
          },
          "addedCount": {
            "type": "integer"
          },
          "added": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "linkedinUrl": {
                  "type": "string",
                  "format": "uri"
                },
                "tracked": {
                  "type": "boolean"
                }
              }
            }
          },
          "skippedCount": {
            "type": "integer"
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "linkedinUrl": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "description": "Machine reason, e.g. already_added, invalid_url, trial_limit."
                },
                "message": {
                  "type": "string",
                  "description": "The same reason in a sentence."
                }
              }
            }
          },
          "seatAllowance": {
            "type": "object",
            "properties": {
              "trialSeatsLeft": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "dailyQuotaLeft": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          }
        }
      },
      "DeletedEmployee": {
        "type": "object",
        "required": [
          "id",
          "deleted"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "deleted": {
            "type": "boolean"
          },
          "permanent": {
            "type": "boolean",
            "description": "True when the record and its history were erased, not just archived."
          }
        }
      },
      "Change": {
        "type": "object",
        "required": [
          "id",
          "organizationId",
          "employeeId",
          "changeType",
          "riskLevel",
          "detectedAt"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "organizationId": {
            "type": "integer"
          },
          "employeeId": {
            "type": "integer"
          },
          "employeeName": {
            "type": [
              "string",
              "null"
            ]
          },
          "changeType": {
            "type": "string",
            "enum": [
              "profile_update",
              "headline_change",
              "title_change",
              "open_to_work",
              "new_skills",
              "location_change",
              "photo_or_summary",
              "edit_activity",
              "experience_change",
              "employee_departed"
            ]
          },
          "changedFields": {
            "type": [
              "string",
              "null"
            ],
            "description": "Raw field diff, e.g. `open_to_work; off; on`."
          },
          "riskScore": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "riskLevel": {
            "$ref": "#/components/schemas/RiskLevel"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ],
            "description": "One sentence explaining what changed."
          },
          "factors": {
            "type": "array",
            "description": "The specific signals that moved the score.",
            "items": {
              "type": "string"
            }
          },
          "followerCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "detectedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmployeePage": {
        "type": "object",
        "required": [
          "items",
          "total",
          "page",
          "pageSize",
          "hasMore"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Employee"
            }
          },
          "total": {
            "type": "integer",
            "description": "Rows matching the filter, across all pages."
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          },
          "hasMore": {
            "type": "boolean",
            "description": "True while further pages exist. Loop until it is false."
          }
        }
      },
      "ChangePage": {
        "type": "object",
        "required": [
          "items",
          "total",
          "page",
          "pageSize",
          "hasMore"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Change"
            }
          },
          "total": {
            "type": "integer",
            "description": "Rows matching the filter, across all pages."
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          },
          "hasMore": {
            "type": "boolean",
            "description": "True while further pages exist. Loop until it is false."
          }
        }
      },
      "CompetitorSummary": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "linkedinId": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "trackedEmployeeCount": {
            "type": "integer"
          }
        }
      },
      "Competitor": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CompetitorSummary"
          },
          {
            "type": "object",
            "properties": {
              "roleFilters": {
                "type": "array",
                "description": "Departments or free text. Empty means the whole roster.",
                "items": {
                  "type": "string"
                }
              },
              "paused": {
                "type": "boolean",
                "description": "Paused workspaces are untracked: no scan, no seats, history kept."
              }
            }
          }
        ]
      },
      "CompetitorCreate": {
        "type": "object",
        "required": [
          "companyLinkedInUrl"
        ],
        "properties": {
          "companyLinkedInUrl": {
            "type": "string",
            "format": "uri",
            "description": "Their LinkedIn company page, e.g. https://www.linkedin.com/company/contoso/."
          },
          "roleFilters": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Departments (\"engineering\", \"sales\", \"product\") or free text (\"enterprise sales\"). Empty imports the whole roster."
          },
          "name": {
            "type": "string",
            "description": "The brand you meant. Checked against the page the URL resolves to."
          },
          "acknowledgeIdentityMismatch": {
            "type": "boolean",
            "default": false,
            "description": "Resend with true to track a page that did not match the name you gave."
          }
        }
      },
      "CompetitorCreated": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "trackedEmployeeCount": {
            "type": "integer"
          },
          "importing": {
            "type": "boolean",
            "description": "True while the roster import is running."
          }
        }
      },
      "CompetitorUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "paused": {
            "type": "boolean"
          },
          "roleFilters": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "DeletedCompetitor": {
        "type": "object",
        "required": [
          "id",
          "deleted"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "deleted": {
            "type": "boolean"
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "url",
          "enabled"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "whe_2c9f1a"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "employeeWarnings": {
            "type": "boolean"
          },
          "employeeWarningSensitivity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "poachingOpportunities": {
            "type": "boolean"
          },
          "poachingOpportunitySensitivity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "consecutiveFailures": {
            "type": "integer"
          },
          "lastSuccessAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastFailureAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpointWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEndpoint"
          },
          {
            "type": "object",
            "required": [
              "secret"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "description": "The HMAC-SHA256 signing secret. Returned once, at creation, and never again.",
                "examples": [
                  "whsec_9f3c..."
                ]
              }
            }
          }
        ]
      },
      "WebhookEndpointCreate": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS endpoint. Private and loopback addresses are refused."
          },
          "description": {
            "type": "string",
            "description": "Label for your own records."
          },
          "employeeWarnings": {
            "type": "boolean",
            "default": true
          },
          "poachingOpportunities": {
            "type": "boolean",
            "default": true
          },
          "employeeWarningSensitivity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "default": 3,
            "description": "1 sends everything, 5 only the strongest signals."
          },
          "poachingOpportunitySensitivity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "default": 3
          }
        }
      },
      "WebhookEndpointUpdate": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "employeeWarnings": {
            "type": "boolean"
          },
          "poachingOpportunities": {
            "type": "boolean"
          },
          "employeeWarningSensitivity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "poachingOpportunitySensitivity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          }
        }
      },
      "DeletedWebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "deleted"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "createdAt",
          "organization",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "evt_18342_9"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "employee.warning",
              "poaching.opportunity",
              "ping"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "sensitivityLevel": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "organization": {
            "$ref": "#/components/schemas/OrganizationRef"
          },
          "data": {
            "type": "object",
            "properties": {
              "competitor": {
                "description": "Present on poaching.opportunity only.",
                "$ref": "#/components/schemas/OrganizationRef"
              },
              "employee": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "title": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "location": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "linkedinUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "profileImageUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Deep link to this person in the dashboard."
                  }
                }
              },
              "change": {
                "$ref": "#/components/schemas/Change"
              }
            }
          }
        }
      }
    }
  }
}
