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
- Add an explicit `api:` field to the entry, e.g. api: https://api.lever.co/v0/postings/<slug>, alongside a human-facing careers_url.
- 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.
- 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.
- 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
- Always pair a corporate careers_url with an explicit api: field when the board is Lever.
- Validate portals.yml entries with provider.detect() before running scans.
- Check the careers page for a jobs.lever.co link to confirm the ATS vendor before adding the entry.
- Watch for companies migrating ATS vendors — detect() returning null is the early signal.
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
- arbeitnow: invalid URL
- breezy: cannot derive API URL for
- collage: cannot derive API URL for
- comeet: invalid URL
- deutschebahn: cannot resolve db.jobs search id for
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)