> ## 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.

# License

> View your license details and license consumption across your organization.

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>;
};

Endor Labs licenses are based on the number of contributing developers and are billed annually. Your license defines the features available to you and the number of contributing developers you are entitled to. See [Endor Labs licenses](/introduction/licenses) for the full licensing model, SKUs, and entitlements per contributing developer.

You can view your product bundle, current plan, expiration date, enabled features, scan credits, and license consumption to monitor usage throughout your contract term and plan for renewals. Only users with the **Admin** role in the root namespace can view license details. See [Authorization roles](/platform-administration/rbac/authorization-roles) to learn about roles.

To view license details:

1. Select **Settings** from the user menu.
2. Select **License**.
3. Select **License Info** from the left sidebar.

<img src="https://mintcdn.com/endorlabs-b4795f4f/8J4Wa_U0h_IBLAS3/images/platform-administration/license/license.webp?fit=max&auto=format&n=8J4Wa_U0h_IBLAS3&q=85&s=e8c1c8b9013cdd74ab3d90c629314cb1" alt="License information" width="1466" height="407" data-path="images/platform-administration/license/license.webp" />

## Contributing developers

License consumption is measured in contributing developers. A contributing developer is any developer who has committed code to a scanned branch in the past 90 days. Each developer is counted once across all namespaces in your tenant, deduplicated by commit email and source code management (SCM) identity. The license consumption count is the total number of unique contributing developers at a point in time. Commits made to pull request branches are not counted.

Click **Download CSV** to download the list of contributing developer email addresses, SCM IDs, host URLs, and verification status as a CSV file. The verification status column shows **Yes** for verified contributors and **No** for unverified ones.

<img src="https://mintcdn.com/endorlabs-b4795f4f/8J4Wa_U0h_IBLAS3/images/platform-administration/license/contributing-developers.webp?fit=max&auto=format&n=8J4Wa_U0h_IBLAS3&q=85&s=bcb7a975d914f07646de706606185b18" alt="Contributing developers" width="1950" height="819" data-path="images/platform-administration/license/contributing-developers.webp" />

Each contributing developer is classified as verified or unverified, which determines how accurately they are counted toward your license consumption.

* **Verified**: A unique developer whose committing email is linked to a confirmed SCM account, such as a GitHub identity. Multiple committing emails linked to the same SCM account are counted as one in the license consumption count.
* **Unverified**: The committing email could not be associated with an SCM identity. This also happens when branches are scanned in CI or CLI mode, where the scan has no access to SCM APIs to resolve identities.

### Organizations that need attention

The **Organizations that need attention** list shows organizations with unverified contributors. Verify these contributors to get an accurate license consumption count and resolve developers who would otherwise be counted incorrectly.

For projects scanned with scheduled, agentless scans, Endor Labs verifies contributors automatically through SCM APIs. Projects scanned through the CLI or in CI need manual verification.

To verify contributors:

1. Click the vertical three dots on the organization you want to verify and select **Verify contributors**.
2. Enter the access token with the required permissions. See [Supported SCM platforms and access tokens](#supported-scm-platforms-and-access-tokens) to learn more.
3. Click **Verify**.

Verification runs in the background and refreshes the status in the list and the contributor count. If verification fails, the status shows an error. Review the error detail for the reason and how to resolve it.

<img src="https://mintcdn.com/endorlabs-b4795f4f/8J4Wa_U0h_IBLAS3/images/platform-administration/license/error.webp?fit=max&auto=format&n=8J4Wa_U0h_IBLAS3&q=85&s=059fc60bb521777b71bb9c45b3cc8367" alt="Verification error" width="794" height="789" data-path="images/platform-administration/license/error.webp" />

The status indicator may show failure even if some projects in the organization are verified but others aren't, due to permission or other issues. Download the CSV to see which projects failed, fix the cause, and verify again.

Some contributors remain unverified even after verification, such as those whose committing emails are local machine addresses, CI/CD bot commits, or emails with typos in the Git configuration. If unverified contributors count remains but no organizations appear in the list, their committing emails could not be linked to any SCM identity. Download the CSV to identify these contributors.

<img src="https://mintcdn.com/endorlabs-b4795f4f/8J4Wa_U0h_IBLAS3/images/platform-administration/license/contributing-developers-noorg.webp?fit=max&auto=format&n=8J4Wa_U0h_IBLAS3&q=85&s=cc97ac8ed2c424de9641533467bfdf4a" alt="Contributing developers only" width="1923" height="770" data-path="images/platform-administration/license/contributing-developers-noorg.webp" />

### Supported SCM platforms and access tokens

To verify contributors, use an access token with the minimum permissions required for your SCM platform, as shown in the following table.

<YamlTable>
  {`

    - SCM_Platform: **GitHub**
    Access_Token: [A GitHub fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) with the **Contents** repository permission set to **Read-only**, which automatically includes read-only access to **Metadata**. Alternatively, you can also use a [GitHub classic personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) with the \`repo\` scope.
    - SCM_Platform: **GitLab**
    Access_Token: [A GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token) with \`read_api\` and \`read_user\` permissions.
    - SCM_Platform: **Bitbucket Cloud**
    Access_Token: [Workspace access token](https://support.atlassian.com/bitbucket-cloud/docs/create-a-workspace-access-token/) or [project access token](https://support.atlassian.com/bitbucket-cloud/docs/create-a-project-access-token/) with at least \`Project: read\` permission.
    - SCM_Platform: **Bitbucket Data Center**
    Access_Token: An [HTTP access token](https://confluence.atlassian.com/bitbucketserver/http-access-tokens-939515499.html) with \`Project: read\` permission.
    - SCM_Platform: **Azure DevOps**
    Access_Token: An [Azure DevOps personal access token](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#create-a-pat) with read permissions in the **Code** and **Graph** categories.

    `}
</YamlTable>

## Scan credits

Your license includes scan credits for each contributing developer, which pool across your contract term. The page shows your scan credit usage and total scan credit pool. Each PR scan, monitored branch scan, and default branch scan above the allowed limit counts against the scans permitted by your contract plan.

For per-seat allocations, credit pooling, and overage, see [Endor Labs licenses](/introduction/licenses#entitlements-and-limits).

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Verification failed with an authentication error. What should I do?">
    Your access token is invalid, expired, or missing permissions. Generate a new token with the [required permissions](#supported-scm-platforms-and-access-tokens) and verify again.
  </Accordion>

  <Accordion title="Why does verification show an error when some projects succeeded?">
    Verification succeeded for some projects and failed for others. Download the CSV to see which projects failed, fix the cause, and verify again.
  </Accordion>

  <Accordion title="Are bots counted toward the contributor count?">
    No. Endor Labs automatically filters known bots, such as Dependabot, Renovate, and GitHub Actions, so they are not counted. Other bots may still appear in the contributor list. Contact your account team for additional information.
  </Accordion>

  <Accordion title="How is the access token handled?">
    Endor Labs clears the token after verification and does not store it. Enter it again each time you verify.
  </Accordion>

  <Accordion title="Why did the contributor count change without anyone doing anything?">
    The count reflects commits from the past 90 days and refreshes periodically. Contributors who have not committed in that window drop out automatically.
  </Accordion>

  <Accordion title="How is a developer who contributes to multiple projects counted?">
    Once. A developer who commits to multiple projects counts only once toward your license consumption.
  </Accordion>

  <Accordion title="Does Endor Labs block scans when total usage is over the scan credit pool?">
    No. Scans continue and any overage appears in your usage. Contact your account team to add more credits.
  </Accordion>

  <Accordion title="Why do some contributors stay unverified even after I run verification and there are no further entries in this section?">
    Their commit emails are not linked to any SCM account, such as local machine addresses, bot commits, or typos in the Git configuration. Download the CSV to find them, then link the email to the SCM account or correct the Git configuration. Contact your account team for additional information.
  </Accordion>
</AccordionGroup>
