santifer/career-ops · error · Error

recruitee: cannot derive API URL for

Error message

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

What it means

Like the pinpoint provider, recruitee's fetch() derives its API URL (https://<slug>.recruitee.com/api/offers/) from the entry's careers_url via resolveApiUrl(). If that returns null — no careers_url, unparseable, non-HTTPS, or non-matching hostname — fetch() throws this error rather than silently producing no jobs.

Solutions

  1. Set careers_url to https://<slug>.recruitee.com for the tenant
  2. If the company uses a custom domain, resolve it (DNS CNAME) to the underlying recruitee.com host and use that
  3. Ensure the URL is valid https: and matches the single-label slug pattern
  4. Confirm the company actually uses Recruitee; otherwise switch providers

Example fix

// before (portals.yml)
careers_url: https://acme.com/careers
// after
careers_url: https://acme.recruitee.com
Defensive patterns

Strategy: validation

Validate before calling

const RE = /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/;
function recruiteeReady(entry) {
  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  if (!raw) return false;
  try { const u = new URL(raw); return u.protocol === 'https:' && RE.test(u.hostname); } catch { return false; }
}

Type guard

function isRecruiteeEntry(entry) {
  return typeof entry?.careers_url === 'string'
    && /^https:\/\/[a-z0-9][a-z0-9-]*\.recruitee\.com\/?$/.test(entry.careers_url);
}

Try / catch

try {
  await recruitee.fetch(entry, ctx);
} catch (err) {
  if (err.message.includes('cannot derive API URL')) {
    console.warn(`Skipping ${entry.name}: careers_url is not a <slug>.recruitee.com URL`);
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: fetch() called for an entry with a missing/empty careers_url, a malformed URL, an http: URL, or a hostname that fails the <slug>.recruitee.com regex (e.g. a custom careers domain).

Common situations: Config entry for a company that left Recruitee (or serves its board on a custom domain); careers_url omitted while the entry is routed to the recruitee provider; typo in the slug.

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/cb53824fba6eb377. Report an issue: GitHub.

Appendix: source

Thrown at providers/recruitee.mjs:53

    return null;
  }
  if (parsed.protocol !== 'https:') return null;
  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) return null;
  return `https://${parsed.hostname}/api/offers/`;
}

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

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

  async fetch(entry, ctx) {
    const apiUrl = resolveApiUrl(entry);
    if (!apiUrl) throw new Error(`recruitee: cannot derive API URL for ${entry.name}`);
    assertRecruiteeUrl(apiUrl);
    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
    return parseRecruiteeResponse(json, entry.name);
  },
};

/**
 * Parse a Recruitee /api/offers/ response. Exported for unit tests.
 *
 * Recruitee returns:
 *   { offers: [{ title, careers_url?, url?, city?, country?, remote?, location? }] }
 *
 * - url: prefer `careers_url`, fall back to `url`. Recruitee tenants commonly
 *   serve postings on their own custom domain (e.g. `careers.hostaway.com`),
 *   so this URL is NOT host-locked to `*.recruitee.com`. Unlike the API
 *   endpoint, the per-offer URL is display-only — it is written to the pipeline
 *   and scan history but never server-fetched here, so the SSRF rationale does
 *   not apply. It is sourced from the already-validated tenant API response.

View on GitHub (pinned to aac998c7ed)