{"record":{"id":"30a0a8ef6ebe9508","repo":"santifer/career-ops","slug":"smartrecruiters-cannot-derive-api-url-for-entry","errorCode":null,"errorMessage":"smartrecruiters: cannot derive API URL for ${entry.name}","messagePattern":"smartrecruiters: cannot derive API URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/smartrecruiters.mjs","lineNumber":124,"sourceCode":"}\n\nfunction resolveApiUrl(entry) {\n  const slug = resolveSlug(entry);\n  return slug ? buildPostingsUrl(slug, 0) : null;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'smartrecruiters',\n\n  detect(entry) {\n    const apiUrl = resolveApiUrl(entry);\n    return apiUrl ? { url: apiUrl } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const slug = resolveSlug(entry);\n    if (!slug) throw new Error(`smartrecruiters: cannot derive API URL for ${entry.name}`);\n\n    // Honor the ctx.maxPages pagination hint (the portal health probe passes 1);\n    // a real sweep keeps the SR_MAX_PAGES safety cap.\n    const pageLimit = Math.min(\n      SR_MAX_PAGES,\n      Number.isInteger(ctx?.maxPages) && ctx.maxPages > 0 ? ctx.maxPages : Infinity,\n    );\n\n    const all = [];\n    for (let page = 0; page < pageLimit; page++) {\n      const apiUrl = buildPostingsUrl(slug, page * SR_PAGE_SIZE);\n      assertSmartRecruitersUrl(apiUrl);\n      const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });\n      const parsed = parseSmartRecruitersResponse(json, entry.name);\n      if (parsed.length === 0) break;\n      all.push(...parsed);\n      if (parsed.length < SR_PAGE_SIZE) break;  // last page (short)\n    }","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/smartrecruiters.mjs#L106-L142","documentation":"The smartrecruiters provider's fetch derives an API slug from entry.api or entry.careers_url via resolveSlug; if neither yields a slug (missing, non-HTTPS, wrong host like a branded domain, or empty path), fetch cannot construct the postings API URL and throws this error. It means the portals entry is not identifiable as a SmartRecruiters tenant.","triggerScenarios":"An entry with provider: smartrecruiters (explicit) but neither api nor a careers.smartrecruiters.com / jobs.smartrecruiters.com URL set; careers_url pointing at a branded custom domain (e.g. https://jobs.continental.com) with no api override; api/careers_url using http:// so resolveSlug skips it.","commonSituations":"Migrating a company entry whose careers page moved to a branded domain and forgetting to pin the slug; typo in the host so SR_CAREERS_HOSTS doesn't match; empty string or wrong type in api field; copy-pasting a tenant id into the wrong config key.","solutions":["Add api: 'https://careers.smartrecruiters.com/<slug>' to the portals entry so the slug is pinned explicitly (this is the documented fix for branded domains)","Fix careers_url to match https://(careers|jobs).smartrecruiters.com/<slug> with a non-empty slug","Ensure the scheme is https:// — resolveSlug silently skips http URLs","Verify the entry actually is a SmartRecruiters tenant; if not, remove or change its provider"],"exampleFix":"// before\n- name: Continental\ncareers_url: 'https://jobs.continental.com'\n// after\n- name: Continental\ncareers_url: 'https://jobs.continental.com'\napi: 'https://careers.smartrecruiters.com/Continental'","handlingStrategy":"validation","validationCode":"function canDeriveSrSlug(entry) {\n  for (const raw of [entry?.api, entry?.careers_url]) {\n    if (typeof raw !== 'string' || !raw) continue;\n    try {\n      const u = new URL(raw);\n      if (u.protocol === 'https:' &&\n          ['careers.smartrecruiters.com','jobs.smartrecruiters.com'].includes(u.hostname) &&\n          u.pathname.split('/').filter(Boolean).length) return true;\n    } catch {}\n  }\n  return false;\n}\nif (!canDeriveSrSlug(entry)) console.error(`${entry.name}: needs api pin or (careers|jobs).smartrecruiters.com URL`);","typeGuard":"const hasSrSlugSource = (entry) =>\n  [entry?.api, entry?.careers_url].some((raw) => {\n    if (typeof raw !== 'string') return false;\n    try {\n      const u = new URL(raw);\n      return u.protocol === 'https:' && /(^|\\.)(careers|jobs)\\.smartrecruiters\\.com$/.test(u.hostname);\n    } catch { return false; }\n  });","tryCatchPattern":"try {\n  await srProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).includes('cannot derive API URL')) {\n    console.error(`Entry ${entry.name}: add api: 'https://careers.smartrecruiters.com/<slug>' to pin the tenant slug`);\n  } else throw e;\n}","preventionTips":["For branded career domains, always add the api key with the SmartRecruiters slug URL","Keep careers_url on (careers|jobs).smartrecruiters.com when possible so detection works without api","Validate provider entries at startup: provider: smartrecruiters must yield a slug","Remember http:// sources are skipped by resolveSlug — use https"],"tags":["config-error","slug-resolution","provider-detection","job-scanner"],"backgroundTag":"missing-required-config-field","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"}