santifer/career-ops · error · Error

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

Error message

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

What it means

In fetch(), resolveApiUrl returns null/empty when it cannot produce an API URL — i.e. entry has neither an explicit api: field nor a careers_url it can derive a Lever slug from. Because detect() already swallows resolveApiUrl's exceptions and returns null, reaching fetch() with no URL means the entry was selected despite yielding no API endpoint.

Source

Thrown at providers/lever.mjs:61

  return `https://api.${host[1]}/v0/postings/${slug}`;
}

/** @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' });
    if (!Array.isArray(json)) return [];
    return json.map(j => ({
      title: j.text || '',
      url: j.hostedUrl || '',
      company: entry.name,
      location: j.categories?.location || '',
      // 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 9b17a8ac97)

Solutions

  1. Add an explicit api: https://api.lever.co/v0/postings/<slug> to the entry in portals.yml.
  2. Or set careers_url: https://jobs.lever.co/<slug> so the slug can be auto-derived.
  3. Confirm the entry should actually use provider: lever (it may belong to greenhouse/ashby).
  4. Ensure the orchestrator only calls fetch() on entries for which detect() returned non-null.

Example fix

# before (portals.yml)
- name: Acme
  provider: lever
  careers_url: https://www.acme.com/careers

# after
- name: Acme
  provider: lever
  api: https://api.lever.co/v0/postings/acme
  careers_url: https://www.acme.com/careers
Defensive patterns

Strategy: validation

Validate before calling

// Verify detect() yields an API URL before calling fetch().
// Provider.detect() returns { url } or null — never call fetch on a null detect.
const detected = provider.detect(entry);
if (!detected || !detected.url) {
  throw new Error(`config: lever entry ${entry.name} has no api URL (set api: or a lever.co careers_url)`);
}

Type guard

/** @param {import('./providers/_types.js').Provider} provider @param {any} entry */
function canFetchLever(provider, entry) {
  return Boolean(provider.detect?.(entry)?.url);
}

Try / catch

try {
  const detected = provider.detect(entry);
  if (!detected?.url) throw new Error(`lever: cannot derive API URL for ${entry.name}`);
  const jobs = await provider.fetch(entry, ctx);
} catch (err) {
  console.warn(`skipping ${entry.name}: ${err.message}`);
}

Prevention

When it happens

Trigger: An entry reaches fetch() (the orchestrator called it directly, or detect returned a truthy {url} that later became empty) but entry.api is unset AND entry.careers_url does not match the jobs.(eu.)?lever.co/<slug> pattern. entry.name is interpolated so you know which portal entry is at fault.

Common situations: A portals.yml entry has provider: lever but neither api: nor a lever.co careers_url; the careers_url host pattern changed; detect() and fetch() were called out of order; the entry name has trailing characters that broke slug extraction.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/138c9463cd637090. Report an issue: GitHub.