santifer/career-ops · error · Error
jibeapply: cannot derive API URL for
Error message
jibeapply: cannot derive API URL for ${entry.name} What it means
After reading entry.careers_url, jibeapply converts it into the board's JSON API URL — either from an explicit entry.api or via toApiUrl(careers_url). When neither yields a usable API URL, fetch() throws this error naming the entry, because scraping cannot proceed without the JSON endpoint.
Solutions
- Set entry.api explicitly in portals.yml to the board's JSON endpoint (validateExplicitApi accepts it)
- Correct careers_url so it matches the jibeapply.com URL pattern toApiUrl expects
- Inspect the board in a browser (devtools network tab) to find the actual API URL and paste it into entry.api
- Remove the entry if the company no longer uses JibeApply
Example fix
// before (portals.yml) - name: acme careers_url: "https://acme-hr.com/jobs" // after - name: acme careers_url: "https://acme-hr.com/jobs" api: "https://acme.jibeapply.com/api/jobs"
Defensive patterns
Strategy: validation
Validate before calling
function canDeriveJibeApi(entry) {
if (typeof entry?.api === 'string' && /^https:\/\//.test(entry.api)) return true;
const url = entry?.careers_url;
if (typeof url !== 'string') return false;
try { return new URL(url).hostname.endsWith('.jibeapply.com'); } catch { return false; }
}
// skip entries where canDeriveJibeApi(entry) is false unless you add entry.api Type guard
const hasDerivableApi = (e) =>
(typeof e?.api === 'string' && e.api.startsWith('https://')) ||
(() => { try { return new URL(e.careers_url).hostname.endsWith('.jibeapply.com'); } catch { return false; } })(); Try / catch
try {
await jibeapplyProvider.fetch(entry, ctx);
} catch (err) {
if (/cannot derive API URL/.test(err.message)) {
console.warn(`add entry.api for ${entry.name}: ${err.message}`);
return [];
}
throw err;
} Prevention
- For branded/custom-domain JibeApply boards, always set entry.api explicitly
- Find the JSON endpoint via browser devtools before adding an entry
- Keep careers_url on the jibeapply.com host when possible so derivation works
- Re-check entries after companies change careers subdomains
When it happens
Trigger: entry.api is absent or fails validateExplicitApi, AND toApiUrl cannot transform the careers_url — e.g. the URL is not a jibeapply.com host or doesn't match the expected path pattern needed to derive the API endpoint.
Common situations: careers_url points at a custom domain whose API URL cannot be guessed (and no entry.api was supplied); the company changed its JibeApply subdomain; a typo in the URL breaks the pattern match.
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
- 4dayweek: unexpected API response on page
- a16z-speedrun-talent: unexpected API response on page
- agentic-jobs: unexpected API response shape on page
- API error
- arbeitnow: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/0317533d81d9e66b.
Report an issue: GitHub.
Appendix: source
Thrown at providers/jibeapply.mjs:105
id: 'jibeapply',
detect(entry) {
const url = entry.careers_url;
if (typeof url !== 'string') return null;
const apiUrl = toApiUrl(url);
if (!apiUrl) return null;
return { url: apiUrl };
},
async fetch(entry, ctx) {
const url = entry.careers_url;
if (typeof url !== 'string' || !url) throw new Error('jibeapply: careers_url required');
// Prefer an explicit entry.api (allows iCIMS-hosted, branded JibeApply sites
// that share the same JSON schema but aren't on jibeapply.com).
const apiUrl = (typeof entry.api === 'string' && validateExplicitApi(entry.api))
|| toApiUrl(url);
if (!apiUrl) throw new Error(`jibeapply: cannot derive API URL for ${entry.name}`);
const first = await ctx.fetchJson(apiUrl, { redirect: 'error' });
const total = first.totalCount ?? 0;
// Use the actual number of items returned as page size — some implementations
// set `count` to the total rather than the per-page count.
const pageSize = first.jobs?.length || first.count || DEFAULT_PAGE_SIZE;
const allJobs = [...(first.jobs ?? [])];
if (total > pageSize && pageSize > 0) {
const maxPages = resolveMaxPages(entry);
const pages = Math.min(Math.ceil(total / pageSize), maxPages);
// Sequential, not concurrent (mirrors providers/4dayweek.mjs, thehub.mjs,
// arbeitnow.mjs, workday.mjs) — a single tenant's API has no reason to
// receive a burst of parallel requests, and a mid-run failure stops
// cleanly with whatever pages were already gathered instead of
// discarding them (Promise.all would fail the whole batch on one error).
for (let page = 2; page <= pages; page++) {
const u2 = new URL(apiUrl);
u2.searchParams.set('page', String(page));View on GitHub (pinned to aac998c7ed)