santifer/career-ops · error · Error

lever: cannot derive API URL for

Error message

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

What it means

The Lever provider can only scrape a company if it can map the portal entry to a Lever postings API URL. It uses an explicit `api:` field if present, otherwise it auto-detects from a careers_url matching jobs.(eu.)lever.co/<slug>. If neither yields a URL, fetch() throws this error naming the entry.

Solutions

  1. Add an explicit `api:` field to the entry, e.g. api: https://api.lever.co/v0/postings/<slug>, alongside a human-facing careers_url.
  2. Fix careers_url to a valid Lever board URL of the form https://jobs.lever.co/<slug> (or jobs.eu.lever.co), with a non-empty slug.
  3. Verify the company actually hosts its board on Lever (check the careers page for a jobs.lever.co link); if it uses Greenhouse/Ashby/etc., switch the entry to that provider.
  4. Confirm detect() returned non-null before calling fetch(): detect() returns null for the same condition instead of throwing.

Example fix

// before (portals.yml)
- name: Coalfire
  careers_url: https://www.coalfire.com/careers
// after
- name: Coalfire
  careers_url: https://www.coalfire.com/careers
  api: https://api.lever.co/v0/postings/coalfire
Defensive patterns

Strategy: validation

Validate before calling

const u = new URL(entry.careers_url || '');
const isLever = /^jobs\.(?:eu\.)?lever\.co$/.test(u.hostname) && u.pathname.split('/').filter(Boolean).length > 0;
if (!entry.api && !isLever) throw new Error(`entry ${entry.name} is not a Lever board; set api: https://api.lever.co/v0/postings/<slug>`);

Type guard

function hasLeverApiUrl(entry) {
  if (entry.api) return true;
  try {
    const u = new URL(entry.careers_url || '');
    return /^jobs\.(?:eu\.)?lever\.co$/.test(u.hostname) && !!u.pathname.split('/').filter(Boolean)[0];
  } catch { return false; }
}

Try / catch

try {
  await leverProvider.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).startsWith('lever: cannot derive API URL')) {
    console.warn(`Skipping ${entry.name}: not a Lever board — add api: or fix careers_url`);
    return [];
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling lever.fetch(entry) where entry has no `api` field and its careers_url is (a) not parseable as a URL, (b) not on a jobs.lever.co / jobs.eu.lever.co host, or (c) on a Lever board host but with an empty path (no slug).

Common situations: portals.yml entry pointing at the company's corporate careers page (e.g. https://www.coalfire.com/careers) without an `api:` override; a typo'd Lever host like jobs.lever.io; a board URL like https://jobs.lever.co/co with a missing or empty slug segment; a company that migrated off Lever.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-22). Data as JSON: /api/errors/138c9463cd637090. Report an issue: GitHub.

Appendix: source

Thrown at providers/lever.mjs:86

  return merged.join('; ');
}

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

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

  async fetch(entry, ctx) {
    const apiUrl = resolveApiUrl(entry);
    if (!apiUrl) throw new Error(`lever: cannot derive API URL for ${entry.name}`);
    assertLeverUrl(apiUrl);
    const json = await ctx.fetchJson(apiUrl, { redirect: 'error', timeoutMs: LEVER_TIMEOUT_MS });
    if (!Array.isArray(json)) return [];
    return json.map(j => ({
      title: j.text || '',
      url: j.hostedUrl || '',
      company: entry.name,
      location: resolveLocation(j.categories),
      // Lever's v0 postings list ships the full description for free (same
      // payload, no per-job request) — enables scan.mjs content_filter.
      description: typeof j.descriptionPlain === 'string' ? j.descriptionPlain : '',
      postedAt: typeof j.createdAt === 'number' ? j.createdAt : undefined,
    }));
  },
};

View on GitHub (pinned to e7abd431fc)