{
  "openapi": "3.1.0",
  "info": {
    "title": "Job Seek API",
    "version": "1.3.0",
    "description": "Public API for AI agents to search jobs, companies, and taxonomies on jseek.co and to create private watchlist handoff links. Anonymous GETs share a pre-cache edge budget of 60 requests per minute per IP with public-read Server Actions. Active requests that execute an API origin additionally use a 30 requests per minute sliding limit. Successful deterministic responses may be served from Vercel's CDN; they omit caller-specific rate-limit headers. The edge may return HTTP 403 when its shared budget is exceeded, while the origin returns a non-cacheable HTTP 429 with reset metadata. Anonymous watchlist discovery is retired: GET /api/v1/watchlists returns a uniform, non-cacheable HTTP 410 through 31 October 2026, then the compatibility route may be removed. Authenticated owner-scoped watchlist list/read access is deferred until verified API identity exists; cross-user discovery is not a replacement contract.\n\nIMPORTANT: Filter parameters (loc, occ, sen, tech) require exact slugs, NOT freetext. Use /api/v1/resolve to convert freetext to slugs first, or /api/v1/taxonomies to list all valid slugs. Passing freetext like loc=Zurich will NOT work — use loc=zurich (the slug)."
  },
  "servers": [{ "url": "https://jseek.co" }],
  "paths": {
    "/api/v1/search": {
      "get": {
        "operationId": "searchJobs",
        "summary": "Search jobs across companies",
        "description": "Returns up to 5 companies with their top 3 job postings matching the filters. Only 'q' accepts freetext. Slug filters (loc, occ, sen, tech) require exact slugs — use /api/v1/resolve to convert freetext to slugs first.\n\nUI ``locale`` (response labels) is distinct from ``lang`` (job document language). Omitting ``lang`` returns postings in any language — pass ``lang=en`` (or a comma-separated list) to filter to specific posting languages. Unsupported locale, language, enum, slug, and range values return HTTP 400 with an ``{\"error\": string}`` body. Provider failures use HTTP 500 and never include provider details.",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Keywords (free text search)" },
          { "name": "loc", "in": "query", "schema": { "type": "string" }, "description": "Location slugs, comma-separated (exact slugs only, use /resolve to look up)" },
          { "name": "occ", "in": "query", "schema": { "type": "string" }, "description": "Occupation slugs, comma-separated (exact slugs only, use /resolve to look up)" },
          { "name": "sen", "in": "query", "schema": { "type": "string" }, "description": "Seniority slugs, comma-separated (exact slugs only, use /resolve to look up)" },
          { "name": "tech", "in": "query", "schema": { "type": "string" }, "description": "Technology slugs, comma-separated (exact slugs only, use /resolve to look up)" },
          { "name": "wm", "in": "query", "style": "form", "explode": false, "schema": { "type": "array", "items": { "type": "string", "enum": ["onsite", "hybrid", "remote"] } }, "description": "Work modes, comma-separated. Unsupported values return HTTP 400." },
          { "name": "etype", "in": "query", "style": "form", "explode": false, "schema": { "type": "array", "items": { "type": "string", "enum": ["full_time", "part_time", "contract", "internship", "temporary", "volunteer"] } }, "description": "Employment types, comma-separated. Unsupported values return HTTP 400." },
          { "name": "sal", "in": "query", "schema": { "type": "string", "pattern": "^\\s*(?:\\d+\\s*-\\s*\\d+|\\d+\\s*-\\s*|-\\s*\\d+)\\s*$", "examples": ["80000-150000", "80000-", "-150000"] }, "description": "Salary range in EUR with non-negative integer bounds. Closed (min-max) and one-sided open ranges (min- or -max) are accepted; at least one bound is required. Omit the parameter for no salary bound." },
          { "name": "exp", "in": "query", "schema": { "type": "string", "pattern": "^\\s*(?:\\d+\\s*-\\s*\\d+|\\d+\\s*-\\s*|-\\s*\\d+)\\s*$", "examples": ["3-10", "3-", "-10"] }, "description": "Experience range in years with non-negative integer bounds. Closed (min-max) and one-sided open ranges (min- or -max) are accepted; at least one bound is required. Omit the parameter for no experience bound." },
          { "name": "lang", "in": "query", "style": "form", "explode": false, "schema": { "type": "array", "items": { "type": "string", "enum": ["en", "de", "fr", "it"] } }, "description": "Job document language filter (comma-separated codes). Distinct from ``locale`` (which controls response labels/currency formatting). Omit to return postings in any language; the API does not fall back to a cookie or to ``locale`` (public APIs are stateless)." },
          { "name": "locale", "in": "query", "schema": { "type": "string", "default": "en", "enum": ["en", "de", "fr", "it"] }, "description": "UI locale for response labels and currency formatting (en, de, fr, it). Does NOT filter postings by document language — use ``lang`` for that." }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "companies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "slug": { "type": "string" },
                          "icon": { "type": "string", "nullable": true },
                          "url": { "type": "string" },
                          "activeJobs": { "type": "integer" },
                          "topPostings": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": { "type": "string", "description": "Posting UUID — use with /api/v1/job to get full details" },
                                "title": { "type": "string", "nullable": true },
                                "location": { "type": "string", "nullable": true },
                                "url": { "type": "string" }
                              }
                            }
                          }
                        }
                      }
                    },
                    "totalCompanies": { "type": "integer" },
                    "moreAt": { "type": "string", "description": "Link to the equivalent full result set on jseek.co. When ``lang`` is omitted, the link carries the UI's ``*`` sentinel to preserve the API's all-language default." }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed or unsupported search parameter. The response is not cacheable.",
            "headers": {
              "Cache-Control": {
                "description": "Always ``no-store`` for error responses.",
                "schema": { "type": "string", "const": "no-store" }
              },
              "Access-Control-Allow-Origin": {
                "description": "Cross-origin access is permitted for public API clients.",
                "schema": { "type": "string", "const": "*" }
              },
              "X-RateLimit-Limit": {
                "description": "Request limit when the rate-limit backend is available.",
                "schema": { "type": "integer" }
              },
              "X-RateLimit-Remaining": {
                "description": "Remaining requests when the rate-limit backend is available.",
                "schema": { "type": "integer" }
              },
              "X-RateLimit-Reset": {
                "description": "Rate-limit reset time in Unix milliseconds when the backend is available.",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "500": { "$ref": "#/components/responses/ProviderUnavailable" },
          "429": {
            "description": "Rate limit exceeded. The response is not cacheable.",
            "headers": {
              "Cache-Control": {
                "description": "Always ``no-store`` for rate-limit responses.",
                "schema": { "type": "string", "const": "no-store" }
              },
              "Access-Control-Allow-Origin": {
                "description": "Cross-origin access is permitted for public API clients.",
                "schema": { "type": "string", "const": "*" }
              },
              "Retry-After": {
                "description": "Seconds until the client may retry.",
                "schema": { "type": "integer", "minimum": 1 }
              },
              "X-RateLimit-Limit": {
                "description": "Request limit for the current window.",
                "schema": { "type": "integer" }
              },
              "X-RateLimit-Remaining": {
                "description": "Remaining requests; always zero for this response.",
                "schema": { "type": "integer", "const": 0 }
              },
              "X-RateLimit-Reset": {
                "description": "Rate-limit reset time in Unix milliseconds.",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": { "error": "Too many requests" }
              }
            }
          }
        }
      }
    },
    "/api/v1/job": {
      "get": {
        "operationId": "getJobDetail",
        "summary": "Get full details for a job posting",
        "description": "Returns structured metadata for a single job posting: salary, technologies, seniority, experience, locations. Does not include the job description text — visit the returned URL on jseek.co to read the full posting.",
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Job posting UUID (from search results topPostings[].id)" },
          { "name": "locale", "in": "query", "schema": { "type": "string", "default": "en", "enum": ["en", "de", "fr", "it"] }, "description": "Response language (en, de, fr, it)" }
        ],
        "responses": {
          "200": {
            "description": "Job posting detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "title": { "type": "string", "nullable": true },
                    "company": {
                      "type": "object",
                      "properties": {
                        "name": { "type": "string" },
                        "slug": { "type": "string" },
                        "icon": { "type": "string", "nullable": true },
                        "url": { "type": "string" }
                      }
                    },
                    "locations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "type": { "type": "string" },
                          "geoType": { "type": "string", "nullable": true },
                          "parentName": { "type": "string", "nullable": true }
                        }
                      }
                    },
                    "seniority": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "slug": { "type": "string" },
                        "name": { "type": "string" }
                      }
                    },
                    "technologies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" }
                        }
                      }
                    },
                    "salary": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "min": { "type": "integer", "nullable": true },
                        "max": { "type": "integer", "nullable": true },
                        "currency": { "type": "string", "nullable": true },
                        "period": { "type": "string", "nullable": true }
                      }
                    },
                    "experience": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "min": { "type": "integer", "nullable": true },
                        "max": { "type": "integer", "nullable": true }
                      }
                    },
                    "employmentType": { "type": "string", "nullable": true },
                    "url": { "type": "string", "description": "Link to view the full posting on jseek.co" },
                    "firstSeenAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "404": { "description": "Job posting not found" },
          "429": { "description": "Rate limit exceeded" }
        }
      }
    },
    "/api/v1/taxonomies": {
      "get": {
        "operationId": "getTaxonomies",
        "summary": "List all values for a taxonomy type",
        "description": "Returns all valid slugs for a taxonomy type. Use these exact slugs in filter params. For locations, use /api/v1/resolve instead (too many to enumerate).",
        "parameters": [
          { "name": "type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["seniority", "occupations", "technologies", "industries"] } },
          { "name": "locale", "in": "query", "schema": { "type": "string", "default": "en", "enum": ["en", "de", "fr", "it"] } }
        ],
        "responses": {
          "200": {
            "description": "Taxonomy items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string" },
                    "items": { "type": "array" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "500": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      }
    },
    "/api/v1/companies": {
      "get": {
        "operationId": "searchCompanies",
        "summary": "Search companies by name",
        "description": "Returns up to 10 companies matching the query. Use company slugs in watchlist creation.",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Company name query (min 2 chars)" },
          { "name": "locale", "in": "query", "schema": { "type": "string", "default": "en", "enum": ["en", "de", "fr", "it"] } }
        ],
        "responses": {
          "200": {
            "description": "Matching companies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "companies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "slug": { "type": "string" },
                          "icon": { "type": "string", "nullable": true },
                          "url": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "500": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      }
    },
    "/api/v1/watchlists": {
      "get": {
        "operationId": "retiredWatchlistDiscovery",
        "summary": "Public watchlist discovery retired",
        "description": "Anonymous watchlist discovery no longer returns watchlist data. During the bounded compatibility window through 31 October 2026, every origin request returns the same non-cacheable 410 response regardless of query parameters, locale, session state, or stored watchlists. The route may be removed after the Sunset instant. Future list/read access is deferred until API authentication can derive and enforce the authenticated owner; client-supplied user identity alone will never authorize access. The separate /api/v1/watchlist/create private handoff remains available.",
        "deprecated": true,
        "parameters": [],
        "responses": {
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "410": {
            "description": "Anonymous public-watchlist discovery is gone. This compatibility response is available only through the documented Sunset instant and reveals no watchlist existence or metadata.",
            "headers": {
              "Cache-Control": {
                "description": "The compatibility response is never cacheable.",
                "schema": { "type": "string", "const": "no-store" }
              },
              "Access-Control-Allow-Origin": {
                "description": "Cross-origin access is permitted for compatibility clients.",
                "schema": { "type": "string", "const": "*" }
              },
              "Sunset": {
                "description": "Last instant of the bounded compatibility window; the route may be removed afterward.",
                "schema": { "type": "string", "const": "Sat, 31 Oct 2026 23:59:59 GMT" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": { "error": "Public watchlist discovery is no longer available" }
              }
            }
          }
        }
      }
    },
    "/api/v1/watchlist/create": {
      "get": {
        "operationId": "createWatchlistLink",
        "summary": "Generate a prefilled watchlist creation link",
        "description": "Returns a URL that opens the watchlist creation page with filters, title, and description prefilled. The user must log in to save. Also returns a preview with matching job/company counts so the agent can verify the filters are useful before handing off. Unsupported locale, enum, and exact-slug values return HTTP 400.",
        "parameters": [
          { "name": "title", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Watchlist title" },
          { "name": "description", "in": "query", "schema": { "type": "string" }, "description": "Watchlist description" },
          { "name": "q", "in": "query", "schema": { "type": "string" } },
          { "name": "loc", "in": "query", "schema": { "type": "string" } },
          { "name": "occ", "in": "query", "schema": { "type": "string" } },
          { "name": "sen", "in": "query", "schema": { "type": "string" } },
          { "name": "tech", "in": "query", "schema": { "type": "string" } },
          { "name": "wm", "in": "query", "style": "form", "explode": false, "schema": { "type": "array", "items": { "type": "string", "enum": ["onsite", "hybrid", "remote"] } }, "description": "Work modes, comma-separated" },
          { "name": "etype", "in": "query", "style": "form", "explode": false, "schema": { "type": "array", "items": { "type": "string", "enum": ["full_time", "part_time", "contract", "internship", "temporary", "volunteer"] } }, "description": "Employment types, comma-separated" },
          { "name": "sal", "in": "query", "schema": { "type": "string" } },
          { "name": "salcur", "in": "query", "schema": { "type": "string" } },
          { "name": "exp", "in": "query", "schema": { "type": "string" } },
          { "name": "companies", "in": "query", "schema": { "type": "string" }, "description": "Company slugs, comma-separated" },
          { "name": "locale", "in": "query", "schema": { "type": "string", "default": "en", "enum": ["en", "de", "fr", "it"] } }
        ],
        "responses": {
          "200": {
            "description": "Prefilled watchlist link with preview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": { "type": "string", "description": "Link to open watchlist creation with prefilled fields" },
                    "preview": {
                      "type": "object",
                      "properties": {
                        "title": { "type": "string" },
                        "description": { "type": "string", "nullable": true },
                        "matchingCompanies": { "type": "integer" },
                        "matchingJobs": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "500": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      }
    },
    "/api/v1/resolve": {
      "get": {
        "operationId": "resolveSlug",
        "summary": "Convert freetext to taxonomy slugs",
        "description": "Resolves freetext input (e.g. 'Zurich', 'machine learning', 'senior') to the exact slugs needed for filter params. ALWAYS call this before using loc/occ/sen/tech params in other endpoints — freetext does NOT work in filters, only exact slugs do.",
        "parameters": [
          { "name": "type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["locations", "occupations", "seniority", "technologies", "industries"] }, "description": "Which taxonomy to search" },
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Freetext query (min 2 chars)" },
          { "name": "locale", "in": "query", "schema": { "type": "string", "default": "en", "enum": ["en", "de", "fr", "it"] }, "description": "Response language" }
        ],
        "responses": {
          "200": {
            "description": "Matching slugs for the query",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string" },
                    "query": { "type": "string" },
                    "matches": {
                      "type": "array",
                      "maxItems": 10,
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": { "type": "string", "description": "Use this value in filter params" },
                          "name": { "type": "string", "description": "Human-readable name" },
                          "type": { "type": "string", "description": "For locations: macro, country, region, or city" },
                          "parentName": { "type": "string", "nullable": true, "description": "For locations: parent region/country" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/EdgeRateLimited" },
          "500": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      }
    }
  },
  "components": {
    "responses": {
      "BadRequest": {
        "description": "A required parameter is missing or a parameter value is unsupported. The response is not cacheable.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "EdgeRateLimited": {
        "description": "The pre-cache Vercel WAF denied this request because the authoritative IP exceeded the shared 60 requests per 60 seconds public-read budget. This response is generated before the application route; its body, CORS headers, and origin rate-limit headers are not part of the JSON API contract. Retry after the fixed window."
      },
      "ProviderUnavailable": {
        "description": "Search provider failure. The message is user-safe, contains no provider detail, and is never cacheable.",
        "headers": {
          "Cache-Control": {
            "description": "Always ``no-store`` for provider failure responses.",
            "schema": { "type": "string", "const": "no-store" }
          },
          "Access-Control-Allow-Origin": {
            "description": "Cross-origin access is permitted for public API clients.",
            "schema": { "type": "string", "const": "*" }
          },
          "X-RateLimit-Limit": {
            "description": "Request limit when the rate-limit backend is available.",
            "schema": { "type": "integer" }
          },
          "X-RateLimit-Remaining": {
            "description": "Remaining requests when the rate-limit backend is available.",
            "schema": { "type": "integer" }
          },
          "X-RateLimit-Reset": {
            "description": "Rate-limit reset time in Unix milliseconds when the backend is available.",
            "schema": { "type": "integer" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" },
            "example": { "error": "Search service unavailable" }
          }
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error"],
        "properties": {
          "error": { "type": "string" }
        }
      }
    }
  }
}
