> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endorlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Endor Labs licenses

> Licensing model, SKUs, and per-seat entitlements for the Endor Labs platform.

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => <tr key={rowKey(row)}>
            {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

export const ScanCreditCalculator = () => {
  const DAYS_PER_YEAR = 365;
  const REPOS_PER_SEAT = 5;
  const MIN_SEATS = 1;
  const MAX_SEATS = 100000;
  const DEFAULT_SEATS = 100;
  const YEAR_OPTIONS = [1, 2, 3, 4, 5];
  const BORDER_RGB = '38, 208, 124';
  const BRAND_GREEN = '#26D07C';
  const TIER_NONE_ID = 'none';
  const OSS_TIER_OPTIONS = [{
    id: TIER_NONE_ID,
    label: 'No OSS',
    creditsPerSeatPerDay: 0,
    blurb: 'Skip Open Source'
  }, {
    id: 'oss-core',
    label: 'OSS Core',
    creditsPerSeatPerDay: 1,
    blurb: '1 scan / contributor / day'
  }, {
    id: 'oss-pro',
    label: 'OSS Pro',
    creditsPerSeatPerDay: 1.5,
    blurb: '1.5 scans / contributor / day'
  }];
  const CODE_TIER_OPTIONS = [{
    id: TIER_NONE_ID,
    label: 'No Code',
    creditsPerSeatPerDay: 0,
    blurb: 'Skip Code'
  }, {
    id: 'code-core',
    label: 'Code Core',
    creditsPerSeatPerDay: 1,
    blurb: '1 scan / contributor / day'
  }, {
    id: 'code-pro',
    label: 'Code Pro',
    creditsPerSeatPerDay: 1.5,
    blurb: '1.5 scans / contributor / day'
  }];
  const LIGHT_THEME = {
    surface: '#ffffff',
    surfaceMuted: '#f6f8fa',
    surfaceSunken: '#ffffff',
    text: '#1f2937',
    textMuted: 'rgba(31,41,55,0.55)',
    textSubtle: 'rgba(31,41,55,0.65)',
    textBody: 'rgba(31,41,55,0.85)',
    border: 'rgba(0,0,0,0.1)',
    borderSubtle: 'rgba(0,0,0,0.06)',
    inputBorder: 'rgba(0,0,0,0.12)',
    inputSurface: '#ffffff',
    shadow: `0 1px 0 rgba(${BORDER_RGB}, 0.1) inset`,
    choiceIdleBg: '#f6f8fa'
  };
  const DARK_THEME = {
    surface: '#0d1117',
    surfaceMuted: '#161b22',
    surfaceSunken: '#0d1117',
    text: '#e6edf3',
    textMuted: 'rgba(230,237,243,0.6)',
    textSubtle: 'rgba(230,237,243,0.65)',
    textBody: 'rgba(230,237,243,0.85)',
    border: 'rgba(255,255,255,0.12)',
    borderSubtle: 'rgba(255,255,255,0.08)',
    inputBorder: 'rgba(255,255,255,0.15)',
    inputSurface: '#161b22',
    shadow: `0 1px 0 rgba(${BORDER_RGB}, 0.05) inset`,
    choiceIdleBg: '#161b22'
  };
  const [ossTierId, setOssTierId] = useState(TIER_NONE_ID);
  const [codeTierId, setCodeTierId] = useState(TIER_NONE_ID);
  const [seats, setSeats] = useState(DEFAULT_SEATS);
  const [years, setYears] = useState(1);
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const checkTheme = () => {
      const root = document.documentElement;
      setIsDark(root.dataset.theme === 'dark' || root.classList.contains('dark'));
    };
    checkTheme();
    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => observer.disconnect();
  }, []);
  const theme = isDark ? DARK_THEME : LIGHT_THEME;
  const findById = (options, id) => options.find(opt => opt.id === id) || options[0];
  const selectedOssTier = findById(OSS_TIER_OPTIONS, ossTierId);
  const selectedCodeTier = findById(CODE_TIER_OPTIONS, codeTierId);
  const safeSeats = Math.max(MIN_SEATS, Math.min(MAX_SEATS, Math.floor(seats || 0)));
  const combinedDailyCredits = selectedOssTier.creditsPerSeatPerDay + selectedCodeTier.creditsPerSeatPerDay;
  const annualCredits = safeSeats * combinedDailyCredits * DAYS_PER_YEAR;
  const totalCredits = annualCredits * years;
  const repositoryCapacity = safeSeats * REPOS_PER_SEAT;
  const formatNumber = value => Math.round(value).toLocaleString('en-US');
  const pluralize = (count, singular, plural) => count === 1 ? singular : plural;
  const formatDailyCredits = value => value % 1 === 0 ? `${value}` : value.toFixed(1);
  const onSeatsChange = event => {
    const parsed = Number.parseInt(event.target.value, 10);
    setSeats(Number.isNaN(parsed) ? 0 : parsed);
  };
  const onYearsChange = event => {
    const parsed = Number.parseInt(event.target.value, 10);
    setYears(Number.isNaN(parsed) ? 1 : parsed);
  };
  const styles = {
    container: {
      background: theme.surface,
      border: `1px solid rgba(${BORDER_RGB}, 0.35)`,
      borderRadius: '14px',
      padding: '1.25rem 1.25rem 1rem',
      boxShadow: theme.shadow,
      margin: '1.25rem 0'
    },
    sectionLabel: {
      fontSize: '0.78rem',
      fontWeight: 600,
      textTransform: 'uppercase',
      letterSpacing: '0.05em',
      color: theme.textMuted,
      marginBottom: '0.5rem'
    },
    choiceGroup: {
      display: 'flex',
      flexWrap: 'wrap',
      gap: '0.5rem',
      marginBottom: '1rem'
    },
    choiceBlurb: {
      display: 'block',
      marginTop: '0.2rem',
      fontWeight: 400,
      fontSize: '0.78rem',
      color: theme.textSubtle
    },
    inputRow: {
      display: 'flex',
      flexWrap: 'wrap',
      gap: '1rem',
      marginBottom: '1rem'
    },
    field: {
      display: 'flex',
      flexDirection: 'column',
      flex: '1 1 160px',
      minWidth: '140px'
    },
    fieldLabel: {
      fontSize: '0.8rem',
      fontWeight: 600,
      color: theme.text,
      marginBottom: '0.35rem'
    },
    input: {
      padding: '0.55rem 0.7rem',
      border: `1px solid ${theme.inputBorder}`,
      background: theme.inputSurface,
      color: theme.text,
      borderRadius: '8px',
      fontSize: '0.9rem',
      fontFamily: 'inherit'
    },
    resultsCard: {
      marginTop: '0.5rem',
      padding: '1rem 1.1rem',
      background: theme.surfaceMuted,
      border: `1px solid rgba(${BORDER_RGB}, 0.25)`,
      borderRadius: '12px'
    },
    resultsGrid: {
      display: 'grid',
      gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
      gap: '0.75rem',
      marginBottom: '0.75rem'
    },
    resultCell: {
      padding: '0.6rem 0.75rem',
      background: theme.surfaceSunken,
      border: `1px solid ${theme.borderSubtle}`,
      borderRadius: '8px'
    },
    resultLabel: {
      fontSize: '0.72rem',
      fontWeight: 600,
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      color: theme.textMuted,
      marginBottom: '0.25rem'
    },
    resultValue: {
      fontSize: '1.25rem',
      fontWeight: 700,
      color: BRAND_GREEN,
      fontVariantNumeric: 'tabular-nums'
    },
    resultUnit: {
      fontSize: '0.75rem',
      fontWeight: 500,
      color: theme.textSubtle,
      marginTop: '0.15rem'
    },
    note: {
      fontSize: '0.85rem',
      color: theme.textBody,
      lineHeight: 1.5
    },
    fieldset: {
      border: 0,
      padding: 0,
      margin: 0
    },
    bundleCard: {
      marginTop: '0.75rem',
      padding: '0.85rem 1rem',
      background: theme.surfaceSunken,
      border: `1px solid ${theme.borderSubtle}`,
      borderRadius: '10px'
    },
    bundleList: {
      listStyle: 'none',
      padding: 0,
      margin: '0.5rem 0 0.5rem',
      display: 'flex',
      flexDirection: 'column',
      gap: '0.4rem'
    },
    bundleItem: {
      display: 'flex',
      flexDirection: 'column',
      padding: '0.45rem 0.6rem',
      background: `rgba(${BORDER_RGB}, 0.06)`,
      border: `1px solid rgba(${BORDER_RGB}, 0.18)`,
      borderRadius: '6px'
    },
    bundleHeader: {
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      flexWrap: 'wrap'
    },
    bundleLabel: {
      fontSize: '0.85rem',
      fontWeight: 600,
      color: BRAND_GREEN
    },
    bundleBlurb: {
      fontSize: '0.78rem',
      color: theme.textSubtle,
      marginTop: '0.1rem'
    },
    bundleNote: {
      fontSize: '0.78rem',
      color: theme.textSubtle,
      margin: 0
    },
    infoPane: {
      padding: '0.65rem 0.9rem',
      margin: '0 0 1.25rem',
      background: `rgba(${BORDER_RGB}, 0.06)`,
      border: `1px solid rgba(${BORDER_RGB}, 0.18)`,
      borderRadius: '6px',
      fontSize: '0.78rem',
      color: theme.textBody,
      lineHeight: 1.5
    }
  };
  const choiceButtonStyle = selected => ({
    flex: '1 1 160px',
    minWidth: '140px',
    textAlign: 'left',
    padding: '0.65rem 0.85rem',
    border: selected ? `1.5px solid ${BRAND_GREEN}` : `1px solid ${theme.border}`,
    background: selected ? `rgba(${BORDER_RGB}, 0.08)` : theme.choiceIdleBg,
    color: theme.text,
    borderRadius: '10px',
    cursor: 'pointer',
    fontSize: '0.88rem',
    fontWeight: selected ? 600 : 500,
    lineHeight: 1.3
  });
  const renderChoiceButton = (opt, currentId, onChange) => {
    const selected = opt.id === currentId;
    return <button key={opt.id} type="button" role="radio" aria-checked={selected} onClick={() => onChange(opt.id)} style={choiceButtonStyle(selected)}>
        {opt.label}
        <span style={styles.choiceBlurb}>{opt.blurb}</span>
      </button>;
  };
  const renderChoiceGroup = (groupLabel, options, currentId, onChange) => <fieldset style={styles.fieldset}>
      <legend style={styles.sectionLabel}>{groupLabel}</legend>
      <div style={styles.choiceGroup} role="radiogroup" aria-label={groupLabel}>
        {options.map(opt => renderChoiceButton(opt, currentId, onChange))}
      </div>
    </fieldset>;
  const renderResultCell = (label, value, unit) => <div style={styles.resultCell}>
      <div style={styles.resultLabel}>{label}</div>
      <div style={styles.resultValue}>{formatNumber(value)}</div>
      <div style={styles.resultUnit}>{unit}</div>
    </div>;
  const yearLabel = pluralize(years, 'year', 'years');
  const buildSelectedTiers = () => [selectedOssTier, selectedCodeTier].filter(tier => tier.id !== TIER_NONE_ID);
  const renderBundleEntry = tier => <li key={tier.id} style={styles.bundleItem}>
      <span style={styles.bundleHeader}>
        <span style={styles.bundleLabel}>{tier.label}</span>
      </span>
      <span style={styles.bundleBlurb}>{tier.blurb}</span>
    </li>;
  const renderStackingNote = () => {
    if (combinedDailyCredits === 0) {
      return <p style={styles.bundleNote}>
          Select at least one license tier to see scan credits.
        </p>;
    }
    const formatted = formatDailyCredits(combinedDailyCredits);
    const unit = pluralize(combinedDailyCredits, 'scan', 'scans');
    return <p style={styles.bundleNote}>
        Daily allocations stack across SKUs.{' '}
        <strong>Combined: {formatted} {unit} / contributor / day.</strong>
      </p>;
  };
  const renderBundle = () => {
    const tiers = buildSelectedTiers();
    return <div style={styles.bundleCard}>
        <div style={styles.sectionLabel}>Your stack</div>
        {tiers.length > 0 ? <ul style={styles.bundleList}>
            {tiers.map(renderBundleEntry)}
          </ul> : null}
        {renderStackingNote()}
      </div>;
  };
  const renderResults = () => <div style={styles.resultsCard}>
      <div style={styles.resultsGrid}>
        {renderResultCell('Total pool', totalCredits, `Scan credits over ${years} ${yearLabel}`)}
        {renderResultCell('Per year', annualCredits, 'Scan credits / year')}
        {renderResultCell('Repository capacity', repositoryCapacity, `Repositories onboardable (${REPOS_PER_SEAT} / seat)`)}
      </div>
      <p style={styles.note}>
        Your full pool is prepaid and available upfront for the contract term. You can draw credits flexibly.
      </p>
      {renderBundle()}
    </div>;
  return <div className="not-prose scc-root" style={styles.container}>
      {renderChoiceGroup('Open Source license', OSS_TIER_OPTIONS, ossTierId, setOssTierId)}
      {renderChoiceGroup('Code license', CODE_TIER_OPTIONS, codeTierId, setCodeTierId)}
      <div style={styles.infoPane}>
        Other products are also priced per Code Contributor per year. Currently, they do not consume scan credits.
      </div>
      <div style={styles.inputRow}>
        <div style={styles.field}>
          <label htmlFor="scc-seats" style={styles.fieldLabel}>Contributor seats</label>
          <input id="scc-seats" type="number" min={MIN_SEATS} max={MAX_SEATS} step={1} value={seats} onChange={onSeatsChange} style={styles.input} aria-label="Number of contributor seats" />
        </div>
        <div style={styles.field}>
          <label htmlFor="scc-years" style={styles.fieldLabel}>Contract length</label>
          <select id="scc-years" value={years} onChange={onYearsChange} style={styles.input} aria-label="Contract length in years">
            {YEAR_OPTIONS.map(option => <option key={option} value={option}>
                {option} {pluralize(option, 'year', 'years')}
              </option>)}
          </select>
        </div>
      </div>
      {renderResults()}
    </div>;
};

Endor Labs is licensed per contributor per year, with **Core** and **Pro** tiers across the **Open Source** and **Code** product lines. Every seat includes a daily scan credit allocation that pools across your contract term, plus support for onboarding multiple repositories.

## License tiers and SKUs

All license tiers are priced **per Code Contributor per year**. A **Code Contributor** is any developer who has made one or more commits to a private source code repository monitored by Endor Labs in the last 90 days.

The current SKU lineup and per-SKU feature lists are maintained on the [Endor Labs pricing page](https://www.endorlabs.com/pricing). That page is the authoritative source for Endor Labs SKUs and licenses. Each license is sold standalone and priced per Code Contributor per year.

<YamlTable>
  {`
    - Product: **Endor Open Source Core** (\`EL-OSS-CORE\`)
    Description: Reachability-based SCA, AI model governance, supply chain attack detection, SBOM/VEX.

    - Product: **Endor Open Source Pro** (\`EL-OSS-PRO\`)
    Description: Everything in OSS Core, plus container reachability, upgrade impact, signing, and GitHub enforcement.

    - Product: **Endor Code Core** (\`EL-CODE-CORE\`)
    Description: SAST and secret detection.

    - Product: **Endor Code Pro** (\`EL-CODE-PRO\`)
    Description: Everything in Code Core, plus agentic SAST with auto-triage and AI security code review.

    - Product: **Endor Package Firewall** (\`EL-OSS-FWAL\`)
    Description: Block malicious and unapproved packages at install.

    - Product: **Endor Patches** (\`EL-PATCHES\`)
    Description: Patched versions of high-risk OSS packages.

    - Product: **Endor SBOM Hub** (\`EL-SBOM\`)
    Description: Manage first and third-party SBOMs.

    - Product: **Endor Coding Agent Governance** (\`EL-AGNT-GOV\`)
    Description: Policies for AI coding agents, MCP servers, and skills.
    `}
</YamlTable>

**Endor Open Source** and **Endor Code** licenses have scan credit allocations based on the license and the number of Code Contributors. If you exhaust your scan credits, you need to buy the add-on license **Endor Additional Code Scans** (`EL-ADD-SCANS`). This extends scan capacity beyond your per-seat scan credit pool. It is sold in buckets of 10,000 scans.

## Entitlements and limits

This section describes how seats, scan credits, repository caps, and overage work across product licenses. The limits described in the following sections are **fair usage** limits, sized to the number of seats you purchase. The limits are generous and you will never be blocked from scanning. Teams running scan-intensive workflows can purchase additional scan credits if they need to, but most organizations won't need to.

### Per-seat scan credit allocation

<YamlTable>
  {`
    - Tier: Open Source Core
    Daily allocation per Code Contributor: 1 additional scan / day

    - Tier: Open Source Pro
    Daily allocation per Code Contributor: 1.5 additional scans / day

    - Tier: Code Core
    Daily allocation per Code Contributor: 1 additional scan / day

    - Tier: Code Pro
    Daily allocation per Code Contributor: 1.5 additional scans / day
    `}
</YamlTable>

<Tip>
  Daily allocations accumulate into a shared pool across your contract term. You can draw from this pool flexibly so that a high-volume PR day isn't blocked as long as the pool has remaining credits.
</Tip>

When a Code Contributor is licensed for more than one tier, daily allocations stack.

For example:

* **OSS Core + Code Core** = 2 additional scans / Code Contributor / day
* **OSS Pro + Code Pro** = 3 additional scans / Code Contributor / day (1.5 + 1.5)
* **OSS Core + Code Pro** = 2.5 additional scans / Code Contributor / day

### Included and additional scans

Each Code Contributor seat supports unlimited default branch scans (typically `main` or `master`) for up to 5 repositories. Beyond that, default branch scans are counted as additional scans.

A non-default branch scan is any scan you trigger against a branch other than your default branch. For example: a feature, release, or hotfix branch.

<YamlTable>
  {`
    - Counts against credits: Pull request scans
    Does not count against credits: Default branch scans (within 5 × seats repo limit)

    - Counts against credits: Non-default branch scans
    Does not count against credits: Local IDE scans (\`--dry-run\`)

    - Counts against credits: MCP scans invoked without \`--dry-run\`
    Does not count against credits: SBOM uploads

    - Counts against credits: Default branch scans for repositories beyond 5 × seats
    Does not count against credits: Container scans (on-premises)

    - Counts against credits: -na-
    Does not count against credits: Binary or package scans
    `}
</YamlTable>

### Credit pooling

Credits pool across the full duration of your contract:

* **Annual billing**: Credits unlock yearly at each renewal anniversary.
* **Prepaid multi-year contracts**: All credits across the full term are available upfront.

For example:

* 100 OSS Core seats × 1 year = **36,500 scan credits** for use during that contract year.
* 100 OSS Pro seats × 1 year = **54,750 scan credits** for use during that contract year (Pro allocates 1.5 / Code Contributor / day).
* 100 OSS Core seats × 3 years prepaid = **109,500 scan credits** available upfront across the full term.

Pools provide flexibility within the contract. High-volume PR days are supported as long as the pool has credits remaining.

### Overage

If your scan credit pool is exhausted, you can purchase additional scans in buckets of 10,000 scans as **Endor Additional Code Scans** (`EL-ADD-SCANS`). The platform continues to function without interruption if you exceed your allocation. Overage is handled through your account team.

### Estimate your scan credit pool

Use the calculator to estimate your scan credit pool for a given license stack, seat count, and contract length.

<ScanCreditCalculator />
