{"record":{"id":"0317533d81d9e66b","repo":"santifer/career-ops","slug":"jibeapply-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"jibeapply: cannot derive API URL for ${entry.name}","messagePattern":"jibeapply: cannot derive API URL for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jibeapply.mjs","lineNumber":105,"sourceCode":"  id: 'jibeapply',\n\n  detect(entry) {\n    const url = entry.careers_url;\n    if (typeof url !== 'string') return null;\n    const apiUrl = toApiUrl(url);\n    if (!apiUrl) return null;\n    return { url: apiUrl };\n  },\n\n  async fetch(entry, ctx) {\n    const url = entry.careers_url;\n    if (typeof url !== 'string' || !url) throw new Error('jibeapply: careers_url required');\n\n    // Prefer an explicit entry.api (allows iCIMS-hosted, branded JibeApply sites\n    // that share the same JSON schema but aren't on jibeapply.com).\n    const apiUrl = (typeof entry.api === 'string' && validateExplicitApi(entry.api))\n      || toApiUrl(url);\n    if (!apiUrl) throw new Error(`jibeapply: cannot derive API URL for ${entry.name}`);\n    const first = await ctx.fetchJson(apiUrl, { redirect: 'error' });\n    const total = first.totalCount ?? 0;\n    // Use the actual number of items returned as page size — some implementations\n    // set `count` to the total rather than the per-page count.\n    const pageSize = first.jobs?.length || first.count || DEFAULT_PAGE_SIZE;\n    const allJobs = [...(first.jobs ?? [])];\n\n    if (total > pageSize && pageSize > 0) {\n      const maxPages = resolveMaxPages(entry);\n      const pages = Math.min(Math.ceil(total / pageSize), maxPages);\n      // Sequential, not concurrent (mirrors providers/4dayweek.mjs, thehub.mjs,\n      // arbeitnow.mjs, workday.mjs) — a single tenant's API has no reason to\n      // receive a burst of parallel requests, and a mid-run failure stops\n      // cleanly with whatever pages were already gathered instead of\n      // discarding them (Promise.all would fail the whole batch on one error).\n      for (let page = 2; page <= pages; page++) {\n        const u2 = new URL(apiUrl);\n        u2.searchParams.set('page', String(page));","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/jibeapply.mjs#L87-L123","documentation":"After reading entry.careers_url, jibeapply converts it into the board's JSON API URL — either from an explicit entry.api or via toApiUrl(careers_url). When neither yields a usable API URL, fetch() throws this error naming the entry, because scraping cannot proceed without the JSON endpoint.","triggerScenarios":"entry.api is absent or fails validateExplicitApi, AND toApiUrl cannot transform the careers_url — e.g. the URL is not a jibeapply.com host or doesn't match the expected path pattern needed to derive the API endpoint.","commonSituations":"careers_url points at a custom domain whose API URL cannot be guessed (and no entry.api was supplied); the company changed its JibeApply subdomain; a typo in the URL breaks the pattern match.","solutions":["Set entry.api explicitly in portals.yml to the board's JSON endpoint (validateExplicitApi accepts it)","Correct careers_url so it matches the jibeapply.com URL pattern toApiUrl expects","Inspect the board in a browser (devtools network tab) to find the actual API URL and paste it into entry.api","Remove the entry if the company no longer uses JibeApply"],"exampleFix":"// before (portals.yml)\n- name: acme\n  careers_url: \"https://acme-hr.com/jobs\"\n// after\n- name: acme\n  careers_url: \"https://acme-hr.com/jobs\"\n  api: \"https://acme.jibeapply.com/api/jobs\"","handlingStrategy":"validation","validationCode":"function canDeriveJibeApi(entry) {\n  if (typeof entry?.api === 'string' && /^https:\\/\\//.test(entry.api)) return true;\n  const url = entry?.careers_url;\n  if (typeof url !== 'string') return false;\n  try { return new URL(url).hostname.endsWith('.jibeapply.com'); } catch { return false; }\n}\n// skip entries where canDeriveJibeApi(entry) is false unless you add entry.api","typeGuard":"const hasDerivableApi = (e) =>\n  (typeof e?.api === 'string' && e.api.startsWith('https://')) ||\n  (() => { try { return new URL(e.careers_url).hostname.endsWith('.jibeapply.com'); } catch { return false; } })();","tryCatchPattern":"try {\n  await jibeapplyProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/cannot derive API URL/.test(err.message)) {\n    console.warn(`add entry.api for ${entry.name}: ${err.message}`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["For branded/custom-domain JibeApply boards, always set entry.api explicitly","Find the JSON endpoint via browser devtools before adding an entry","Keep careers_url on the jibeapply.com host when possible so derivation works","Re-check entries after companies change careers subdomains"],"tags":["config","url-derivation","api","provider"],"backgroundTag":"invalid-url","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}