{"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":98,"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":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/jibeapply.mjs#L80-L116","documentation":"Thrown by jibeapply fetch() when neither an explicit entry.api (validated by validateExplicitApi, which requires https) nor a derivable API URL from careers_url (toApiUrl requires https AND a hostname matching ^[a-z0-9-]+\\.jibeapply\\.com$) yields a usable endpoint. fetch() prefers entry.api so branded/iCIMS-hosted JibeApply sites work, then falls back to deriving from a jibeapply.com careers_url; both failing means no API endpoint is reachable.","triggerScenarios":"careers_url is on a branded custom domain (not *.jibeapply.com) AND entry.api is missing, non-https, or malformed; entry.api is set to an http URL (validateExplicitApi rejects non-https); careers_url hostname has an invalid character for the jibeapply.com regex.","commonSituations":"An iCIMS-acquired JibeApply tenant running on a branded domain where someone set careers_url to the branded URL but forgot the matching api: field; setting api: to a plain-http internal endpoint; a typo in the careers_url host breaking the regex.","solutions":["For branded (non-jibeapply.com) JibeApply sites, set both careers_url (any non-empty string, see error 210) AND api: to the https JSON endpoint (e.g. https://careers.acme.com/api/jobs).","For standard tenants, ensure careers_url is the https://<slug>.jibeapply.com form so toApiUrl can derive /api/jobs.","Confirm api: uses https: — validateExplicitApi rejects anything else.","Verify with detect(): if provider.detect(entry) returns null and you have no valid entry.api, fetch() will throw this."],"exampleFix":"# before (branded domain, no api)\n- name: Acme\n  provider: jibeapply\n  careers_url: https://careers.acme.com/jobs\n\n# after\n- name: Acme\n  provider: jibeapply\n  careers_url: https://careers.acme.com/jobs\n  api: https://careers.acme.com/api/jobs","handlingStrategy":"validation","validationCode":"// Mirror fetch()'s resolution: try explicit api (https), then derive from jibeapply.com.\nfunction resolveJibeapplyApi(entry) {\n  if (typeof entry.api === 'string') {\n    try { const u = new URL(entry.api); if (u.protocol === 'https:') return u.href; } catch {}\n  }\n  if (typeof entry.careers_url === 'string') {\n    try {\n      const u = new URL(entry.careers_url);\n      if (u.protocol === 'https:' && /^[a-z0-9-]+\\.jibeapply\\.com$/i.test(u.hostname)) {\n        return new URL('/api' + u.pathname, u.origin).href;\n      }\n    } catch {}\n  }\n  return null;\n}","typeGuard":"/** True when a JibeApply API endpoint is derivable from the entry. */\nfunction isJibeapplyDerivable(entry) {\n  return resolveJibeapplyApi(entry) !== null;\n}","tryCatchPattern":"try {\n  const jobs = await jibeapplyProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/cannot derive API URL/.test(err.message)) {\n    console.error(`config: ${entry.name} — ${err.message} (set api: https://... or a *.jibeapply.com careers_url)`);\n  } else {\n    throw err;\n  }\n}","preventionTips":["For branded (non-jibeapply.com) JibeApply sites, always set BOTH careers_url and an https api:.","Ensure entry.api uses https: — validateExplicitApi rejects anything else.","For standard tenants, use the https://<slug>.jibeapply.com careers_url form."],"tags":["config","url-resolution","jibeapply","ats"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}