{
  "openapi": "3.1.0",
  "info": {
    "title": "LinkFetch API",
    "version": "1",
    "summary": "LinkedIn profiles, companies, jobs, posts, groups, search and outbound as typed JSON.",
    "description": "Every endpoint returns the same envelope: `data` with the typed record(s) and `meta` with request_id, credits_charged, rate_limit and a provenance stamp (source, fetched_at, freshness_days). Credits are flat per request; empty results are never charged. Jobs and locations read from LinkFetch's own index and need only an API key; profile, company, post, group, search and outbound calls resolve through the end user's own LinkedIn session via the Chrome extension.",
    "termsOfService": "https://linkfetch.io/terms",
    "contact": {
      "name": "LinkFetch",
      "url": "https://linkfetch.io/contact",
      "email": "info@linkfetch.io"
    }
  },
  "externalDocs": {
    "url": "https://linkfetch.io/docs",
    "description": "LinkFetch documentation"
  },
  "servers": [
    {
      "url": "https://api.linkfetch.io"
    }
  ],
  "tags": [
    {
      "name": "Profiles",
      "description": "The person, the whole résumé."
    },
    {
      "name": "Companies",
      "description": "Size, funding, headcount trend."
    },
    {
      "name": "Jobs",
      "description": "Our scraped LinkedIn jobs dataset."
    },
    {
      "name": "Posts",
      "description": "Single posts with reactions and counts."
    },
    {
      "name": "Groups",
      "description": "Niche communities inside LinkedIn."
    },
    {
      "name": "Search",
      "description": "Faceted discovery across the graph."
    },
    {
      "name": "Locations",
      "description": "Resolve to LinkedIn geo IDs."
    },
    {
      "name": "Outbound",
      "description": "Act on the graph, with consent."
    },
    {
      "name": "Messaging",
      "description": "Read your inbox, reply into existing threads."
    }
  ],
  "paths": {
    "/v1/profiles": {
      "get": {
        "operationId": "profiles-get",
        "summary": "Get Profile",
        "description": "Full profile for a LinkedIn member by slug or URL.\n\nReads our cache. If the row isn't cached, returns 422 with a hint to run the LinkFetch Chrome extension on a signed-in tab — the extension will post the capture to /v1/profiles/ingest.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Profiles"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": false,
            "description": "A linkedin.com/in/<slug> profile URL.",
            "schema": {
              "type": "string",
              "format": "url"
            },
            "example": "https://www.linkedin.com/in/reidhoffman/"
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "description": "Bare public identifier. Supply one of `url` / `slug`.",
            "schema": {
              "type": "string"
            },
            "example": "reidhoffman"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/companies/{slug}": {
      "get": {
        "operationId": "companies-get",
        "summary": "Get Company",
        "description": "Full company entity by universal-name slug.\n\nResolves through the source router. Cache hits return instantly; misses ask the extension to capture `/voyager/api/organization/companies` on a consented session.\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Companies"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Company universalName (e.g. `stripe`, `vercel`).",
            "schema": {
              "type": "string"
            },
            "example": "stripe"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/profiles/ingest": {
      "post": {
        "operationId": "profiles-ingest",
        "summary": "Ingest Profile",
        "description": "Extension-side write path. Reference only — not runnable from here.\n\nCalled by the LinkFetch Chrome extension after it captures a profile from a signed-in LinkedIn tab. Upserts the row and debits credits atomically. GDPR-suppressed slugs return 410.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Profiles"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "profile"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "url",
                    "description": "Canonical linkedin.com/in/<slug> URL."
                  },
                  "profile": {
                    "type": "string",
                    "format": "Profile",
                    "description": "Normalised payload — see packages/shared/src/profile.ts."
                  },
                  "fetched_at": {
                    "type": "string",
                    "format": "ISO-8601",
                    "description": "When the extension captured the page. Defaults to now."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/profiles/{slug}/posts": {
      "get": {
        "operationId": "profiles-posts",
        "summary": "Get Profile Posts",
        "description": "Recent posts from a profile.\n\nCache-first read off the per-user profile-posts cache. On miss the playground triggers the extension to fetch the dash-graphql `feedDashProfileUpdatesByMemberShareFeed` page on the user's signed-in tab, normalize, and POST to `/v1/profiles/{slug}/posts/ingest`. Each item carries `activity_id`, `ugc_post_urn`, reaction/comment/share counts, and `posted_at` derived from the activity-id snowflake.\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Profiles"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Public identifier (the tail of /in/<slug>).",
            "schema": {
              "type": "string"
            },
            "example": "reidhoffman"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/companies/by-id/{id}": {
      "get": {
        "operationId": "companies-get-by-id",
        "summary": "Get Company (by id)",
        "description": "Company lookup by numeric LinkedIn ID (migration pending).\n\nStub — returns 501. Mirrors `GET /v1/companies/:slug` but addressed by numeric ID (useful when you only have the ID from People search or Company Posts).\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Companies"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric LinkedIn company ID.",
            "schema": {
              "type": "number"
            },
            "example": "2135371"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/companies/{slug}/posts": {
      "get": {
        "operationId": "companies-posts",
        "summary": "Get Company Posts",
        "description": "A company's organization timeline.\n\nCache-first read. On miss the playground triggers the extension to fetch the legacy REST `feed/updates?q=chronFeed` page on the user's signed-in tab, normalize, and POST to `/v1/companies/{slug}/posts/ingest`. Items carry `activity_id`, `ugc_post_urn`, reaction/comment/share counts, and `posted_at` derived from the activity-id snowflake.\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Companies"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Company universalName.",
            "schema": {
              "type": "string"
            },
            "example": "stripe"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/companies/{slug}/employees": {
      "get": {
        "operationId": "companies-employees",
        "summary": "Get Company Employees",
        "description": "People who list this company as current employer.\n\nCache-first read. On miss, returns 422 and the extension can capture the page in one click.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Companies"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Company universalName.",
            "schema": {
              "type": "string"
            },
            "example": "stripe"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (1–50).",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset (0–1000).",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/posts/{id}": {
      "get": {
        "operationId": "posts-get",
        "summary": "Get Post",
        "description": "Normalized post detail — text, author, reaction breakdown, counts.\n\nCache-first. `id` is the numeric `activity:<id>` from `/feed/update/urn:li:activity:<id>/` URLs. On cache miss returns 422 and the extension captures via voyager `feed/updatesV2` on your signed-in LinkedIn tab, normalizes to the `PostPayload` shape, and POSTs to `/ingest`. Response includes `ugc_post_urn` (needed for reactions/comments), `posted_at` (derived from the activity ID's snowflake timestamp), reaction breakdown by type (LIKE / PRAISE / EMPATHY / …), and view/impression counts when LinkedIn exposes them.\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Posts"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric activity ID.",
            "schema": {
              "type": "number"
            },
            "example": "7381045634461569024"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/posts/{id}/reactions": {
      "get": {
        "operationId": "posts-reactions",
        "summary": "Get Post Reactions",
        "description": "Members who reacted to a post.\n\nCache-first read. On miss the playground triggers the extension to round-trip `feed/updatesV2` (resolve `activity:<id>` → `ugcPost:<id>`) and then fetch the `voyagerSocialDashReactions` page on the user's signed-in tab, normalize, and POST to `/v1/posts/{id}/reactions/ingest`. Each item carries reactor URN + slug + name + headline + reaction type.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Posts"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric activity ID.",
            "schema": {
              "type": "number"
            },
            "example": "7381045634461569024"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/groups/{id}": {
      "get": {
        "operationId": "groups-get",
        "summary": "Get Group",
        "description": "Group entity — metadata, owner, rules.\n\nCache-first read. On miss the playground triggers the extension to fetch the dash-graphql `voyagerGroupsDashGroups` endpoint on the user's signed-in tab, normalize, and POST to `/v1/groups/{id}/ingest`. Returns name, description, member count, privacy, owner URN, etc. Viewer-restricted: members-only groups need the user to be a member.\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Groups"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric group ID (from /groups/<id>/).",
            "schema": {
              "type": "number"
            },
            "example": "62436"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/groups/{id}/members": {
      "get": {
        "operationId": "groups-members",
        "summary": "Get Group Members",
        "description": "Admins + members visible to the viewer.\n\nCache-first read. On miss the playground triggers the extension to fetch one page of `voyagerGroupsDashGroupMemberships` filtered to OWNER / MANAGER / MEMBER, normalize, and POST to `/v1/groups/{id}/members/ingest`. Each item carries profile URN + slug + name + headline + role.\n\nCost: 3 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Groups"
        ],
        "x-credits": 3,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric group ID.",
            "schema": {
              "type": "number"
            },
            "example": "62436"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (max ~50).",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/search/people": {
      "get": {
        "operationId": "search-people",
        "summary": "Search People",
        "description": "People search over LinkedIn.\n\nCache-first read keyed off `(facet, query)`. On miss the playground triggers the extension to fetch one page of `voyagerSearchDashClusters` with `resultType=PEOPLE`, normalize, and POST to `/v1/search/people/ingest`. Returns profile URN + slug + name + headline + location per hit.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Search"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Keyword query.",
            "schema": {
              "type": "string"
            },
            "example": "ada lovelace"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/search/companies": {
      "get": {
        "operationId": "search-companies",
        "summary": "Search Companies",
        "description": "Company search.\n\nCache-first read. On miss the playground triggers the extension to fetch one page of `voyagerSearchDashClusters` with `resultType=COMPANIES`, normalize, and POST to `/v1/search/companies/ingest`. Returns company URN + slug + name + industry + member count per hit.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Search"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Keyword query.",
            "schema": {
              "type": "string"
            },
            "example": "payments"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/search/groups": {
      "get": {
        "operationId": "search-groups",
        "summary": "Search Groups",
        "description": "Group search.\n\nCache-first read. On miss the playground triggers the extension to fetch one page of `voyagerSearchDashClusters` with `resultType=GROUPS`, normalize, and POST to `/v1/search/groups/ingest`. Returns group id + name + member count + logo per hit.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Search"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Keyword query.",
            "schema": {
              "type": "string"
            },
            "example": "startup"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/search/posts": {
      "get": {
        "operationId": "search-posts",
        "summary": "Search Posts",
        "description": "Content/posts search.\n\nCache-first read. On miss the playground triggers the extension to fetch one page of `voyagerSearchDashClusters` with `resultType=CONTENT`, normalize, and POST to `/v1/search/posts/ingest`. Each hit carries activity URN + ugcPost URN + commentary + author + reaction/comment/share counts.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Search"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Keyword query.",
            "schema": {
              "type": "string"
            },
            "example": "stripe"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/outbound/connections": {
      "post": {
        "operationId": "outbound-send-connection",
        "summary": "Send Connection Request",
        "description": "Invite a member to connect by slug or profile URL (experimental).\n\nInvites a LinkedIn member to connect via the extension on the user's signed-in session. Pass the vanity slug (e.g. `jdoe`) or a full `/in/` URL — the extension resolves the internal profile URN for you via a cheap `identity/dash/profiles` lookup, then issues the voyager `verifyQuotaAndCreateV2` mutation. The backend route is a 501 stub — use the Capture button to run end-to-end through the extension. The `note` variant (invite with custom message) isn't wired yet; shape pending capture.\n\nCost: 10 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 10,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "Recipient's LinkedIn slug (`jdoe`) or full profile URL (`https://www.linkedin.com/in/jdoe/`). The extension resolves this to a profile URN internally."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/messages": {
      "post": {
        "operationId": "outbound-send-message",
        "summary": "Send DM",
        "description": "Send a direct message to a 1st-degree connection (experimental).\n\nSends a direct message via the extension on the user's signed-in session. Pass the vanity slug or `/in/` URL — the extension resolves it to a profile URN, **verifies the target is a 1st-degree connection**, and only then issues the voyager `createMessage` mutation. Non-1st-degree targets return `409 not_connected` instead — LinkFetch refuses stranger DMs to avoid message-request caps and ban risk. Send a connection request first if you're not connected yet.\n\nCost: 10 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 10,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug",
                  "body"
                ],
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "Recipient's LinkedIn slug (`jdoe`) or full profile URL (`https://www.linkedin.com/in/jdoe/`). The extension resolves this to a profile URN internally."
                  },
                  "body": {
                    "type": "string",
                    "description": "Message text. Plain text with newlines; LinkedIn strips Markdown."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/connections/withdraw": {
      "post": {
        "operationId": "outbound-withdraw-invitation",
        "summary": "Withdraw Invitation",
        "description": "Retract a pending connection request (shape pending capture).\n\nStub. Voyager path confirmed alive: `DELETE /voyager/api/voyagerRelationshipsDashMemberRelationships/<invitation-urn>`. Request body shape pending a UI capture (next session). The invitation URN comes from the Sent-Invitations page — not the same as the invitee's profile URN.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "invitationUrn"
                ],
                "properties": {
                  "invitationUrn": {
                    "type": "string",
                    "description": "`urn:li:fsd_invitation:<id>` — fetched from the sent-invitations listing. The underlying voyager call is `DELETE`, but the LinkFetch wrapper takes a POST."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/posts/{activityId}/reactions": {
      "post": {
        "operationId": "outbound-react-post",
        "summary": "React to Post",
        "description": "Like / praise / empathy / interest / appreciation / entertainment / funny.\n\nAdds a reaction to a LinkedIn post via the extension on the user's session. Voyager path: graphql mutation `voyagerSocialDashReactions.<hash>` with `{entity: {reactionType}, threadUrn: urn:li:activity:<id>}`. Modern posts use the activity URN directly as threadUrn — no ugcPost namespace split. The backend route is a 422 extension_required stub — Run fires the Capture path end-to-end via the extension.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "activityId",
            "in": "path",
            "required": true,
            "description": "Numeric activity id from `/feed/update/<id>` URLs, or full `urn:li:activity:<id>`.",
            "schema": {
              "type": "string"
            },
            "example": "7381045634461569024"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "reactionType": {
                    "type": "string",
                    "description": "One of `LIKE | PRAISE | EMPATHY | INTEREST | APPRECIATION | ENTERTAINMENT | FUNNY`. Defaults to `LIKE`."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/posts/{activityId}/comments": {
      "post": {
        "operationId": "outbound-comment-post",
        "summary": "Comment on Post",
        "description": "Add a top-level comment to a post.\n\nPosts a top-level comment on a LinkedIn post via the extension. Voyager path: `POST /voyager/api/voyagerSocialDashNormComments?decorationId=…NormComment-43`. Body: `{commentary: {text, attributesV2: [], $type: TextViewModel}, threadUrn: urn:li:activity:<id>}`. Reply-to-comment threading is a separate surface (not wired yet).\n\nCost: 10 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 10,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "activityId",
            "in": "path",
            "required": true,
            "description": "Numeric activity id or full `urn:li:activity:<id>`.",
            "schema": {
              "type": "string"
            },
            "example": "7381045634461569024"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "Comment text. Plain text; no LinkedIn rich formatting."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/follows/member": {
      "post": {
        "operationId": "outbound-follow-member",
        "summary": "Follow Member",
        "description": "Follow a LinkedIn member.\n\nFollows a member via the extension. Voyager path: `POST /voyager/api/feed/dash/followingStates/urn:li:fsd_followingState:urn:li:fsd_profile:<tail>` with PATCH body `{patch: {$set: {following: true}}}`. The profile UI moved to SDUI but this voyager endpoint is still alive — LinkFetch bypasses SDUI entirely. Pass a full `urn:li:fsd_profile:<tail>` or just the opaque tail. For `following: false` (unfollow) use the matching entry.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target"
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "Profile URN — full `urn:li:fsd_profile:<tail>` or just the `ACoAA...` tail."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/follows/member/remove": {
      "post": {
        "operationId": "outbound-unfollow-member",
        "summary": "Unfollow Member",
        "description": "Stop following a member.\n\nSame voyager endpoint as Follow Member, toggled: PATCH body `{patch: {$set: {following: false}}}`.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target"
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "Profile URN — full `urn:li:fsd_profile:<tail>` or just the tail."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/follows/company": {
      "post": {
        "operationId": "outbound-follow-company",
        "summary": "Follow Company",
        "description": "Follow a company page.\n\nSame voyager endpoint as Follow Member, with a company URN target. Pass the numeric company id or full `urn:li:fsd_company:<id>`.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target"
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "Company URN — `urn:li:fsd_company:<id>` or just the numeric id (e.g. `2135371` for Stripe)."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/follows/company/remove": {
      "post": {
        "operationId": "outbound-unfollow-company",
        "summary": "Unfollow Company",
        "description": "Stop following a company page.\n\nSame endpoint toggled to `following: false`.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target"
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "Company URN or numeric id."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/posts/{ugcPostId}/shares": {
      "post": {
        "operationId": "outbound-share-post",
        "summary": "Share / Repost",
        "description": "Repost an existing post (shape pending capture).\n\nStub. Two voyager paths probed and both alive — we'll prefer the dash route: `POST /voyager/api/contentcreation/normShares`. Legacy REST `POST /voyager/api/feed/updates?action=createReshare` also responds but is likely retiring. Body pending capture.\n\nCost: 5 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 5,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "ugcPostId",
            "in": "path",
            "required": true,
            "description": "Numeric id of the source post's ugcPost URN.",
            "schema": {
              "type": "string"
            },
            "example": "7381045614270332928"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "commentary": {
                    "type": "string",
                    "description": "Optional commentary to attach to the repost."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outbound/posts/{activityId}/save": {
      "post": {
        "operationId": "outbound-save-post",
        "summary": "Save Post",
        "description": "Bookmark a post to the viewer's saved items.\n\nPATCH-style state update: `POST /voyager/api/voyagerFeedDashSaveStates/<urn:li:fsd_saveState:(SAVE,urn:li:activity:<id>)>` with `{patch: {$set: {saved: true}}}`. Pass `saved: false` via the Unsave entry to remove the bookmark.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "activityId",
            "in": "path",
            "required": true,
            "description": "Numeric activity id from `/feed/update/<id>` URLs or full `urn:li:activity:<id>`.",
            "schema": {
              "type": "string"
            },
            "example": "7381045634461569024"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/outbound/posts/{activityId}/save/remove": {
      "post": {
        "operationId": "outbound-unsave-post",
        "summary": "Unsave Post",
        "description": "Remove a post from the viewer's saved items.\n\nSame save-state endpoint toggled to `saved: false`.\n\nCost: 2 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Outbound"
        ],
        "x-credits": 2,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "activityId",
            "in": "path",
            "required": true,
            "description": "Numeric activity id or full `urn:li:activity:<id>`.",
            "schema": {
              "type": "string"
            },
            "example": "7381045634461569024"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/locations/search": {
      "get": {
        "operationId": "locations-search",
        "summary": "Search Locations",
        "description": "Resolve a city / region string to a canonical LinkedIn geo ID.\n\nType-ahead-style location resolution. Returns LinkedIn's Microsoft Bing Geo identifiers, the same IDs you pass into `/v1/jobs?geo_id=…` and other geo-filtered endpoints. Same query hits LinkedIn at most once per TTL globally; subsequent calls are served from the per-query cache.\n\nCost: 1 credit.",
        "tags": [
          "Locations"
        ],
        "x-credits": 1,
        "x-extension-required": false,
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "City, region, or country text. Free-text.",
            "schema": {
              "type": "string"
            },
            "example": "San Francisco Bay Area"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (1–20).",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "fresh",
            "in": "query",
            "required": false,
            "description": "Skip the cache and force a live LinkedIn typeahead call.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/jobs": {
      "get": {
        "operationId": "jobs-search",
        "summary": "Search Jobs",
        "description": "Filter our LinkedIn job dataset by keyword, location, company, salary, and more.\n\nEvery filter is optional — combine freely. `q` is a full-text keyword. Use `location` (free-text) OR `geo_id` (canonical from `/v1/locations`) for place filters. `posted_within` (24h/week/month) is a shortcut; `posted_after`/`posted_before` give precise bounds (YYYY-MM-DD or ISO-8601). Results paginate via `limit` (max 50) and `offset`.\n\nCost: 1 credit.",
        "tags": [
          "Jobs"
        ],
        "x-credits": 1,
        "x-extension-required": false,
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Keyword search across title and description.",
            "schema": {
              "type": "string"
            },
            "example": "staff engineer"
          },
          {
            "name": "location",
            "in": "query",
            "required": false,
            "description": "Free-text location substring (matches `jobs.location`).",
            "schema": {
              "type": "string"
            },
            "example": "San Francisco Bay Area"
          },
          {
            "name": "geo_id",
            "in": "query",
            "required": false,
            "description": "Canonical LinkedIn geo ID (resolve via `/v1/locations` first). Preferred over `location` when you need consistent matching.",
            "schema": {
              "type": "string",
              "format": "numeric"
            },
            "example": "90000084"
          },
          {
            "name": "company",
            "in": "query",
            "required": false,
            "description": "Exact company-name match (case-insensitive).",
            "schema": {
              "type": "string"
            },
            "example": "Stripe"
          },
          {
            "name": "company_id",
            "in": "query",
            "required": false,
            "description": "Numeric LinkedIn company ID (more reliable than `company`).",
            "schema": {
              "type": "number"
            },
            "example": "2135371"
          },
          {
            "name": "industry_ids",
            "in": "query",
            "required": false,
            "description": "Comma-separated numeric LinkedIn industry IDs.",
            "schema": {
              "type": "string",
              "format": "csv"
            },
            "example": "4,6"
          },
          {
            "name": "employment_type",
            "in": "query",
            "required": false,
            "description": "e.g. `full_time`, `contract`, `part_time`, `internship`.",
            "schema": {
              "type": "string"
            },
            "example": "full_time"
          },
          {
            "name": "level",
            "in": "query",
            "required": false,
            "description": "Seniority bucket — e.g. `entry`, `mid_senior`, `director`.",
            "schema": {
              "type": "string"
            },
            "example": "mid_senior"
          },
          {
            "name": "workplace_type",
            "in": "query",
            "required": false,
            "description": "Onsite / remote / hybrid.",
            "schema": {
              "type": "string",
              "enum": [
                "onsite",
                "remote",
                "hybrid"
              ]
            }
          },
          {
            "name": "easy_apply",
            "in": "query",
            "required": false,
            "description": "Only jobs with LinkedIn Easy Apply enabled.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "has_offsite_apply",
            "in": "query",
            "required": false,
            "description": "Only jobs that link out to an external ATS.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "salary_min",
            "in": "query",
            "required": false,
            "description": "Minimum salary floor — matches rows where `salary_min >= value`.",
            "schema": {
              "type": "number"
            },
            "example": "150000"
          },
          {
            "name": "posted_within",
            "in": "query",
            "required": false,
            "description": "Shortcut date window.",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "week",
                "month"
              ]
            }
          },
          {
            "name": "posted_after",
            "in": "query",
            "required": false,
            "description": "Lower bound on posted date (YYYY-MM-DD or ISO-8601).",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-04-01"
          },
          {
            "name": "posted_before",
            "in": "query",
            "required": false,
            "description": "Upper bound on posted date.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-04-24"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort order.",
            "schema": {
              "type": "string",
              "enum": [
                "recent",
                "oldest"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (1–50).",
            "schema": {
              "type": "number"
            },
            "example": "10"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Row offset for pagination.",
            "schema": {
              "type": "number"
            },
            "example": "0"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/jobs/by-url": {
      "get": {
        "operationId": "jobs-by-url",
        "summary": "Get Job by URL",
        "description": "Resolve any LinkedIn job URL to our dataset row.\n\nAccepts `/jobs/view/<id>/`, `/jobs/collections/…/currentJobId=<id>`, a bare `urn:li:fsJobPosting:<id>`, or a bare numeric ID. Returns the full detail row when we've scraped it — 404 otherwise (the scraper may not have seen it yet; retry in a few hours).\n\nCost: 1 credit.",
        "tags": [
          "Jobs"
        ],
        "x-credits": 1,
        "x-extension-required": false,
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "The LinkedIn job URL, URN, or numeric ID.",
            "schema": {
              "type": "string",
              "format": "url"
            },
            "example": "https://www.linkedin.com/jobs/view/3892101234/"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "operationId": "jobs-get",
        "summary": "Get Job",
        "description": "Full job detail by numeric LinkedIn job ID.\n\nReturns the complete stored row — title, company, description (plain + HTML), salary range, geo, industry, employment type, posted timestamps, Easy Apply flags, social meta. Single-call endpoint; no pagination.\n\nCost: 1 credit.",
        "tags": [
          "Jobs"
        ],
        "x-credits": 1,
        "x-extension-required": false,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Numeric LinkedIn job ID (6+ digits).",
            "schema": {
              "type": "number"
            },
            "example": "3892101234"
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/messaging/conversations": {
      "get": {
        "operationId": "messaging-list-conversations",
        "summary": "List Conversations",
        "description": "Your inbox — paginated, filterable by category.\n\nReturns the user's conversation list for the chosen category. Response is normalized: `{conversations: [...], nextCursor}` — each conversation has plain-string names/headlines, a pre-resolved photo URL, the latest-message preview inline, a string array of `disabledFeatures`, and the `distance` of every participant. Pemberly markup (bold/italic/list/hyperlink) is collapsed to a flat `attributes: [{start, length, kind, url?}]` array. Paginate via `nextCursor` from the prior page. Categories: PRIMARY_INBOX (Focused), SECONDARY_INBOX (Other), INBOX (combined), ARCHIVE, MESSAGE_REQUEST_PENDING, SPAM, INMAIL. Voyager queryId: `messengerConversations.95010742…` on /voyagerMessagingGraphQL/graphql.\n\nCost: 0 credits (extension). Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Messaging"
        ],
        "x-credits": 0,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Inbox tab. Defaults to PRIMARY_INBOX (Focused).",
            "schema": {
              "type": "string",
              "enum": [
                "PRIMARY_INBOX",
                "SECONDARY_INBOX",
                "INBOX",
                "ARCHIVE",
                "MESSAGE_REQUEST_PENDING",
                "SPAM",
                "INMAIL"
              ]
            }
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Page size (1–50). Defaults to 20.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "nextCursor",
            "in": "query",
            "required": false,
            "description": "Pagination cursor — pass the prior page's `metadata.nextCursor` value verbatim.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lastUpdatedBefore",
            "in": "query",
            "required": false,
            "description": "Alternative cursor — page conversations updated before this epoch-ms. Mutually exclusive with `nextCursor`.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/messaging/mailbox-counts": {
      "get": {
        "operationId": "messaging-get-mailbox-counts",
        "summary": "Get Mailbox Counts",
        "description": "Unread-conversation count per category.\n\nPowers the global '(N) Messaging' badge. Returns `{counts: [{category, unreadConversationCount}]}` — one row per category. Cheap polling signal; far lighter than re-listing every category. Voyager queryId: `messengerMailboxCounts.fc528a5a…`.\n\nCost: 0 credits (extension). Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Messaging"
        ],
        "x-credits": 0,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/messaging/conversations/{thread}/messages": {
      "get": {
        "operationId": "messaging-list-messages",
        "summary": "List Messages in a Conversation",
        "description": "Thread history — newest 20, or scroll back from a timestamp.\n\nReturns `{messages, newSyncToken, shouldClearCache, deletedUrns}` — flat shape over the underlying voyager response. Each message has plain-text `text`, sender URN, delivered timestamp, `attachments[]` (sponsored ad / video / image / file with the kind discriminator collapsed), and pemberly `attributes[]` for rich formatting.\n\nLeave `before` empty to get the newest 20 messages. To page back, copy the oldest message's `deliveredAt` and pass it as `before` — the server returns up to 20 messages strictly older than that timestamp. Empty `messages` array ⇒ no more history. The `newSyncToken` in the response is for incremental polling — pass it to the bridge directly when you want only deltas.\n\nCost: 0 credits (extension). Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Messaging"
        ],
        "x-credits": 0,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "thread",
            "in": "path",
            "required": true,
            "description": "Either the short `2-…` thread id (from `/messaging/thread/<id>/` or an inbox row's `threadId`) or a full `urn:li:msg_conversation:(...)`. The bridge fills in your mailbox URN automatically when you give just the thread id.",
            "schema": {
              "type": "string"
            },
            "example": "2-MWI3MDUxOGItOWMzZi00ZGQ5LWJiMWEtODI1YjY3ZGYzN2ZkXzEwMA=="
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Epoch-ms cursor — copy the oldest message's `deliveredAt` to load the previous page. Leave empty for the newest 20.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "post": {
        "operationId": "messaging-reply",
        "summary": "Reply in Conversation",
        "description": "Send a message into an existing thread (Variant C body).\n\nReplies into an existing conversation. Uses the captured 'Variant C' body shape — `conversationUrn` lives inside `message`, no `hostRecipientUrns` needed; LinkedIn routes by the thread URN. Distinct from `/v1/outbound/messages` (Send DM), which is for *new* conversations and takes a recipient slug. The voyager POST is `voyagerMessagingDashMessengerMessages?action=createMessage` with `content-type: text/plain;charset=UTF-8`. Returns `value.entityUrn` (new message URN), `value.conversationUrn`, and `value.deliveredAt`.\n\nCost: 10 credits. Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Messaging"
        ],
        "x-credits": 10,
        "x-extension-required": true,
        "parameters": [
          {
            "name": "thread",
            "in": "path",
            "required": true,
            "description": "Either the short `2-…` thread id (from `/messaging/thread/<id>/` or an inbox row's `threadId`) or a full `urn:li:msg_conversation:(...)`. The bridge fills in your mailbox URN automatically when you give just the thread id.",
            "schema": {
              "type": "string"
            },
            "example": "2-MWI3MDUxOGItOWMzZi00ZGQ5LWJiMWEtODI1YjY3ZGYzN2ZkXzEwMA=="
          }
        ],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Message text. Plain text with newlines; LinkedIn strips Markdown."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/messaging/mark-as-read": {
      "post": {
        "operationId": "messaging-mark-as-read",
        "summary": "Mark Messages as Read",
        "description": "Mailbox-wide mark-as-seen by `until` timestamp.\n\nMarks every message in the user's mailbox with `deliveredAt <= until` as seen — what the LinkedIn UI fires when the inbox is opened. Defaults to `Date.now()`. Per-conversation mark-as-read paths exist on voyager (`?action=read|markAsRead`) but their body shapes are pending capture, so this mailbox-wide call is the v0 primitive. Voyager path: `voyagerMessagingDashMessagingBadge?action=markAllMessagesAsSeen`, body `{until: <epoch-ms>}`.\n\nCost: 0 credits (extension). Resolves through the user's own LinkedIn session via the LinkFetch Chrome extension; returns 422 extension_required on a cache miss.",
        "tags": [
          "Messaging"
        ],
        "x-credits": 0,
        "x-extension-required": true,
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success envelope: `data` plus `meta` (request_id, credits_charged, rate_limit, provenance).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer key."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "Nothing found. Not charged."
          },
          "422": {
            "description": "extension_required — capture the page via the extension and retry."
          },
          "429": {
            "description": "Rate limited. Retry after the window in the response headers."
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "until": {
                    "type": "number",
                    "description": "Epoch-ms. Server marks every message with `deliveredAt <= until` as seen. Defaults to current time."
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from the LinkFetch dashboard."
      }
    },
    "schemas": {
      "Envelope": {
        "type": "object",
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "description": "The typed record or array of records for this endpoint."
          },
          "meta": {
            "type": "object",
            "properties": {
              "request_id": {
                "type": "string"
              },
              "credits_charged": {
                "type": "integer"
              },
              "rate_limit": {
                "type": "object",
                "properties": {
                  "remaining": {
                    "type": "integer"
                  },
                  "reset_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              },
              "provenance": {
                "type": "object",
                "properties": {
                  "source": {
                    "type": "string"
                  },
                  "fetched_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "freshness_days": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}