santifer/career-ops · error

ashby: cannot derive API URL for

Error message

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

What it means

In the Ashby provider's fetch(), resolveApiUrl returned null, meaning no API URL could be derived from the portal entry (no explicit api field and no recognizable board URL to build one from). The provider refuses to guess and throws rather than issuing a request to an unknown endpoint.

Solutions

  1. Find the company's actual Ashby board URL (e.g. https://jobs.ashby.co/<org>) and add it — or its API URL — to the entry.
  2. Set the entry's explicit `api:` field to the Ashby postings endpoint; resolveApiUrl honors it directly.
  3. If the company is not actually on Ashby, move it to the correct provider entry (Greenhouse/Lever/etc.) or a generic scraper entry.
  4. Validate entries at config-load time: skip or warn on entries where resolveApiUrl(entry) is null instead of failing mid-run.

Example fix

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

// after
{ name: 'ExampleCo', careers_url: 'https://exampleco.com/careers', api: 'https://api.ashbyhq.com/posting-api/board/exampleco' }
Defensive patterns

Strategy: fallback

Validate before calling

function hasAshbyApiUrl(entry) {
  return Boolean(entry?.api) || resolveApiUrl(entry) != null;
}

Type guard

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

Try / catch

try {
  return await ashbyProvider.fetch(entry, ctx);
} catch (err) {
  if (/cannot derive API URL/.test(err.message)) {
    console.warn(`Entry ${entry.name} has no derivable Ashby API URL — check portals.yml`);
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling fetch(entry) with an entry lacking both an `api` field and a board URL resolveApiUrl recognizes — e.g. an entry with only `careers_url: https://exampleco.com/careers` (a non-Ashby corporate page) or an empty/blank entry.

Common situations: portals.yml entry for a company that claims Ashby but only lists its corporate careers page; entries where the Ashby org slug was dropped; entries copied from a non-Ashby provider's config shape.

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

Appendix: source

Thrown at providers/ashby.mjs:181

  return [...new Set(parts)].join(' · ');
}

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

  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(`ashby: cannot derive API URL for ${entry.name}`);
    assertAshbyUrl(apiUrl);
    // Shared retry rather than a local loop (#3072). The local one caught
    // EVERY error, so a board that is gone was asked three times: 404, 401 and
    // 410 each bought a second and third request that could only fail again.
    // withRetry stops on the first non-retryable status via isRetryableError,
    // and 22% of Ashby boards are permanently 404 (#2840) — re-probing those
    // is the traffic that provokes the single-host throttle in #2839.
    //
    // It also honours Ashby's own Retry-After on a 429, which the local
    // backoff discarded, while CLAMPING it so a misconfigured
    // `Retry-After: 86400` cannot stall a sweep.
    //
    // ASHBY_RETRIES is passed through as the policy, so the attempt budget is
    // unchanged — only which errors are worth spending it on. The longer
    // per-request timeout above still applies: it is the Ashby latency floor
    // this provider was given a bespoke timeout for, and it travels as `opts`.
    const json = /** @type {any} */ (await fetchJsonWithRetry(
      ctx,

View on GitHub (pinned to aac998c7ed)