santifer/career-ops · error · Error

smartrecruiters: cannot derive API URL for

Error message

smartrecruiters: cannot derive API URL for ${entry.name}

What it means

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.

Solutions

  1. 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)
  2. Fix careers_url to match https://(careers|jobs).smartrecruiters.com/<slug> with a non-empty slug
  3. Ensure the scheme is https:// — resolveSlug silently skips http URLs
  4. Verify the entry actually is a SmartRecruiters tenant; if not, remove or change its provider

Example fix

// before
- name: Continental
careers_url: 'https://jobs.continental.com'
// after
- name: Continental
careers_url: 'https://jobs.continental.com'
api: 'https://careers.smartrecruiters.com/Continental'
Defensive patterns

Strategy: validation

Validate before calling

function canDeriveSrSlug(entry) {
  for (const raw of [entry?.api, entry?.careers_url]) {
    if (typeof raw !== 'string' || !raw) continue;
    try {
      const u = new URL(raw);
      if (u.protocol === 'https:' &&
          ['careers.smartrecruiters.com','jobs.smartrecruiters.com'].includes(u.hostname) &&
          u.pathname.split('/').filter(Boolean).length) return true;
    } catch {}
  }
  return false;
}
if (!canDeriveSrSlug(entry)) console.error(`${entry.name}: needs api pin or (careers|jobs).smartrecruiters.com URL`);

Type guard

const hasSrSlugSource = (entry) =>
  [entry?.api, entry?.careers_url].some((raw) => {
    if (typeof raw !== 'string') return false;
    try {
      const u = new URL(raw);
      return u.protocol === 'https:' && /(^|\.)(careers|jobs)\.smartrecruiters\.com$/.test(u.hostname);
    } catch { return false; }
  });

Try / catch

try {
  await srProvider.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).includes('cannot derive API URL')) {
    console.error(`Entry ${entry.name}: add api: 'https://careers.smartrecruiters.com/<slug>' to pin the tenant slug`);
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/30a0a8ef6ebe9508. Report an issue: GitHub.

Appendix: source

Thrown at providers/smartrecruiters.mjs:124

}

function resolveApiUrl(entry) {
  const slug = resolveSlug(entry);
  return slug ? buildPostingsUrl(slug, 0) : null;
}

/** @type {Provider} */
export default {
  id: 'smartrecruiters',

  detect(entry) {
    const apiUrl = resolveApiUrl(entry);
    return apiUrl ? { url: apiUrl } : null;
  },

  async fetch(entry, ctx) {
    const slug = resolveSlug(entry);
    if (!slug) throw new Error(`smartrecruiters: cannot derive API URL for ${entry.name}`);

    // Honor the ctx.maxPages pagination hint (the portal health probe passes 1);
    // a real sweep keeps the SR_MAX_PAGES safety cap.
    const pageLimit = Math.min(
      SR_MAX_PAGES,
      Number.isInteger(ctx?.maxPages) && ctx.maxPages > 0 ? ctx.maxPages : Infinity,
    );

    const all = [];
    for (let page = 0; page < pageLimit; page++) {
      const apiUrl = buildPostingsUrl(slug, page * SR_PAGE_SIZE);
      assertSmartRecruitersUrl(apiUrl);
      const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
      const parsed = parseSmartRecruitersResponse(json, entry.name);
      if (parsed.length === 0) break;
      all.push(...parsed);
      if (parsed.length < SR_PAGE_SIZE) break;  // last page (short)
    }

View on GitHub (pinned to aac998c7ed)