santifer/career-ops · error · Error

pinpoint: cannot derive API URL for

Error message

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

What it means

The pinpoint provider only serves portal entries whose careers_url matches `https://<slug>.pinpointhq.com`. resolveApiUrl() returns null when the entry has no careers_url, an unparseable URL, a non-HTTPS URL, or a hostname that does not match the tenant regex. fetch() throws this error so the caller knows the provider cannot be used for this entry rather than silently returning no jobs.

Solutions

  1. Set entry.careers_url to the actual tenant subdomain URL, e.g. https://<slug>.pinpointhq.com
  2. Verify the company still uses Pinpoint by checking the careers page; if it migrated, change the provider entry accordingly
  3. Ensure the URL is https: and parseable (no spaces, full scheme included)
  4. If the tenant uses a custom domain, find the underlying <slug>.pinpointhq.com host and use that

Example fix

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

Strategy: validation

Validate before calling

import { URL } from 'url';
const RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.pinpointhq\.com$/;
function resolvable(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 isPinpointEntry(entry) {
  return typeof entry?.careers_url === 'string'
    && /^https:\/\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.pinpointhq\.com\/?$/.test(entry.careers_url);
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('pinpoint: cannot derive API URL')) {
    console.warn(`Skipping ${entry.name}: careers_url is not a Pinpoint tenant URL`);
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling fetch() with an entry whose careers_url is missing, empty, not a string, malformed (new URL() throws), using http: instead of https:, or on a non-pinpointhq.com hostname (e.g. a custom careers domain or a different ATS).

Common situations: A portals.yml entry lists a company that moved off Pinpoint (ATS migration) but still points at the old careers URL; a custom careers domain (careers.example.com) is used instead of the <slug>.pinpointhq.com subdomain; the URL was typoed or entered as http://.

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

Appendix: 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 aac998c7ed)