santifer/career-ops · error · Error

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

Error message

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

What it means

fetch() calls resolveApiUrl(entry) and throws when it returns null. resolveApiUrl returns null if entry.careers_url is missing, not a string, unparseable by the URL constructor, not HTTPS, or its hostname fails PINPOINT_HOST_RE. Unlike the assert* guards (which throw on a URL you already have), this fires during fetch when the entry itself lacks the data needed to build the API URL at all.

Source

Thrown at providers/pinpoint.mjs:64

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

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

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

  async fetch(entry, ctx) {
    const apiUrl = resolveApiUrl(entry);
    if (!apiUrl) throw new Error(`pinpoint: cannot derive API URL for ${entry.name}`);
    assertPinpointUrl(apiUrl);
    // redirect:'error' prevents SSRF via server-side redirects
    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
    return parsePinpointResponse(json, entry.name);
  },
};

/**
 * Parse a Pinpoint /postings.json response. Exported for unit tests.
 *
 * Pinpoint returns:
 *   { data: [{ title, url, path, location: { name, city, province, ... }, ... }] }
 *
 * Field mapping → the normalized Job shape:
 *   - title:    `title`, trimmed.
 *   - url:      `url` — an absolute posting URL on the tenant's own
 *               `<slug>.pinpointhq.com` host. It is display-only (written to the
 *               pipeline and scan history, never server-fetched here), so it is

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Ensure the entry passed to fetch has a valid string careers_url that is https://<slug>.pinpointhq.com.
  2. If you are calling fetch() directly, call detect(entry) first and skip when it returns null — detect uses the same resolveApiUrl and returns null cleanly without throwing.
  3. Check the config source (e.g. portals.yml) for a typo'd or missing careers_url field for this entry name.
  4. If the board genuinely has no pinpointhq.com URL, do not use the pinpoint provider for this entry.

Example fix

// before — fetch called on an entry detect() would reject
await provider.fetch({ name: 'Acme' }, ctx);
// after — guard with detect()
if (provider.detect(entry)) {
  await provider.fetch(entry, ctx);
}
Defensive patterns

Strategy: validation

Validate before calling

// detect() uses the same resolveApiUrl and returns null instead of throwing
const detected = provider.detect(entry);
if (!detected) {
  console.warn(`skip ${entry.name}: pinpoint cannot derive API URL`);
  continue;
}
await provider.fetch(entry, ctx);

Type guard

null

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (e) {
  if (/cannot derive API URL/.test(e.message)) {
    console.warn(`[skip] ${entry.name}: no pinpointhq.com careers_url — fix config`);
  } else throw e;
}

Prevention

When it happens

Trigger: entry.careers_url is undefined/null/empty; entry.careers_url is a non-HTTPS URL (http://acme.pinpointhq.com); entry.careers_url hostname is a branded domain that is not *.pinpointhq.com; entry.careers_url is a malformed string that throws inside new URL().

Common situations: A job_boards row was created without a careers_url (detect() returned null but fetch was still invoked directly); a YAML/JSON config field was misnamed (career_url vs careers_url); the entry was constructed programmatically and the URL field was dropped by a serializer.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/d700a701729c7c16. Report an issue: GitHub.