santifer/career-ops · error

bamboohr: cannot derive API URL for

Error message

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

What it means

BambooHR's fetch() calls resolveOrigin(entry) to get the tenant origin `https://<tenant>.bamboohr.com`; if it returns null the provider cannot build the `/careers/list` endpoint and throws. Like Ashby's equivalent, it refuses to request anything when config is insufficient.

Solutions

  1. Add/set the entry's origin or api field to `https://<tenant>.bamboohr.com`.
  2. Fix the tenant subdomain so the hostname matches `<tenant>.bamboohr.com` (this also makes resolveOrigin succeed).
  3. Verify the company actually hosts jobs on BambooHR; otherwise move the entry to the right provider.
  4. Validate entries at load time: reject entries where resolveOrigin(entry) is null with a clear config warning.

Example fix

// before
{ name: 'ExampleCo', careers_url: 'https://exampleco.com/careers' }
// fetch throws: cannot derive API URL

// after
{ name: 'ExampleCo', api: 'https://exampleco.bamboohr.com/careers/list' }
Defensive patterns

Strategy: fallback

Validate before calling

function hasBambooOrigin(entry) {
  return resolveOrigin(entry) != null;
}

Type guard

function isBambooReadyEntry(entry) {
  return typeof entry?.name === 'string' && typeof resolveOrigin(entry) === 'string';
}

Try / catch

try {
  return await bamboohrProvider.fetch(entry, ctx);
} catch (err) {
  if (/cannot derive API URL/.test(err.message)) {
    console.warn(`Entry ${entry.name}: missing <tenant>.bamboohr.com origin in config`);
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling fetch(entry) with an entry lacking a usable origin — no api/careers URL matching the BambooHR tenant pattern, or a blank tenant field — so resolveOrigin returns null.

Common situations: portals.yml entry where only the corporate site is listed, tenant subdomain typo making the host pattern fail, or entries migrated from another provider without a BambooHR origin field.

Understand the failure class

Background: "missing required config value" errors: why libraries refuse to start when a configuration key is empty, unset, or blank — this error's family across 48 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/a06a50c7ba0ead2e. Report an issue: GitHub.

Appendix: source

Thrown at providers/bamboohr.mjs:67

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

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

  detect(entry) {
    const origin = resolveOrigin(entry);
    return origin ? { url: `${origin}/careers/list` } : null;
  },

  async fetch(entry, ctx) {
    const origin = resolveOrigin(entry);
    if (!origin) throw new Error(`bamboohr: cannot derive API URL for ${entry.name}`);
    const apiUrl = `${origin}/careers/list`;
    assertBambooHRUrl(apiUrl);
    // redirect:'error' + the host check above keep the final hostname pinned to
    // the tenant — a server-side redirect can't bounce us off-domain (SSRF).
    const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { redirect: 'error' }));
    return parseBambooHRResponse(json, entry.name, origin);
  },
};

/**
 * Parse a BambooHR `/careers/list` response. Exported for unit tests.
 *
 * BambooHR returns:
 *   { meta: {...}, result: [{ id, jobOpeningName,
 *       location: { city?, state? }, isRemote?, employmentStatusLabel? }] }
 *
 * - url: built as `<origin>/careers/<id>` — matches the public
 *   `jobOpeningShareUrl`. Rows without a non-empty `id` are dropped (no stable

View on GitHub (pinned to aac998c7ed)