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
- 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.
- Set the entry's explicit `api:` field to the Ashby postings endpoint; resolveApiUrl honors it directly.
- If the company is not actually on Ashby, move it to the correct provider entry (Greenhouse/Lever/etc.) or a generic scraper entry.
- 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
- Confirm each Ashby-claimed company actually has a jobs.ashby.co board before adding the entry.
- Always populate the explicit `api:` field when the board URL is ambiguous.
- Validate all entries with resolveApiUrl at startup and list offenders.
- Keep provider assignment accurate when companies migrate ATS vendors.
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
- bamboohr: cannot derive API URL for
- arbeitnow: invalid URL
- arbeitsagentur: entry
- ashby: invalid URL
- avature: cannot resolve origin for
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)