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

  1. Set entry.api explicitly in portals.yml to the board's JSON endpoint (validateExplicitApi accepts it)
  2. Correct careers_url so it matches the jibeapply.com URL pattern toApiUrl expects
  3. Inspect the board in a browser (devtools network tab) to find the actual API URL and paste it into entry.api
  4. 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

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


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)