static async apiSearch (req, res, next) {
    if (!ArkimeUtil.isString(req.body.query)) {
      return res.send({ purpose: 'error', text: 'Missing query' });
    }

    if (req.body.tags !== undefined) {
      if (!Array.isArray(req.body.tags)) {
        return res.send({ purpose: 'error', text: 'tags must be an array when present' });
      }
      if (req.body.tags.some(t => typeof t !== 'string')) {
        return res.send({ purpose: 'error', text: 'every tag must be a string' });
      }
    }

    if (req.body.doIntegrations !== undefined) {
      if (!Array.isArray(req.body.doIntegrations)) {
        return res.send({ purpose: 'error', text: 'doIntegrations must be an array when present' });
      }
      if (req.body.doIntegrations.some(i => typeof i !== 'string')) {
        return res.send({ purpose: 'error', text: 'every doIntegration must be a string' });
      }
    }

    if (req.body.viewId !== undefined && !ArkimeUtil.isString(req.body.viewId)) {
      return res.send({ purpose: 'error', text: 'viewId must be a string when present' });
    }

    // dedupe and trim queries
    const queries = [...new Set(
      ArkimeUtil.sanitizeStr(req.body.query.trim())
        .split(/[ |,\t]/)
        .map(query => query.trim())
        .filter(query => query.length > 0)
    )];

    if (queries.length === 0) {
      return res.send({ purpose: 'error', text: 'query must contain at least one non-whitespace indicator' });
    }

    const indicators = queries.map((query) => {
      const { itype, decoded } = Integration.classify(query);
      return { query, itype, decoded };
    });

    const isBulk = indicators.length > 1;

    const issuedAt = Date.now();
    const shared = {
      skipCache: !!req.body.skipCache,
      skipChildren: !!req.body.skipChildren,
      doIntegrations: req.body.doIntegrations,
      user: req.user,
      res,
      sent: 0,
      total: 0, // runIntegrationsList will fix
      resultCount: 0, // sum of _cont3xt.count from results
      queriedSet: new Set(),
      finished: false,
      finishWrite () { // end data write and create audit log when finished
        res.write(JSON.stringify({ purpose: 'finish', resultCount: this.resultCount }));
        res.end(']\n');

        Audit.create({
          userId: req.user.userId,
          indicator: queries.join(', '),
          iType: isBulk ? 'bulk' : indicators[0].itype,
          tags: req.body.tags ?? [],
          viewId: req.body.viewId,
          issuedAt,
          took: Date.now() - issuedAt,
          resultCount: this.resultCount
        }).catch((err) => {
          console.log('ERROR - creating audit log.', err);
        });
      }
    };
    // the total number of integrations to query for the root indicators alone
    const initialTotal = indicators.reduce((total, indicator) => {
      return total + Integration.#integrations[indicator.itype].length;
    }, 0);
    res.write('[\n');
    res.write(JSON.stringify({ purpose: 'init', indicators, sent: shared.sent, total: initialTotal, text: 'more to follow' }));
    res.write(',\n');

    for (const indicator of indicators) {
      const { query, itype } = indicator;
      const integrations = Integration.#integrations[itype];

      Integration.runIntegrationsList(shared, indicator, undefined, integrations);
      if (!shared.skipChildren) {
        if (itype === 'email') {
          const dquery = query.slice(query.indexOf('@') + 1);
          Integration.runIntegrationsList(shared, { query: dquery, itype: 'domain' }, indicator, Integration.#integrations.domain);
        } else if (itype === 'url') {
          const url = new URL(query);
          if (Integration.classify(url.hostname).itype === 'ip') {
            Integration.runIntegrationsList(shared, { query: url.hostname, itype: 'ip' }, indicator, Integration.#integrations.ip);
          } else {
            const equery = await extractDomain(query, { tld: true });
            Integration.runIntegrationsList(shared, { query: equery, itype: 'domain' }, indicator, Integration.#integrations.domain);
          }
        }
      }
    }
  }
POST - /api/integration/search

Fetches integration data
@name /integration/search
@param {string} query - The string to query integrations
@param {string[]} doIntegrations - A list of integration names to query
@param {boolean} skipCache - Ignore any cached data and query all integrations again
@param {boolean} skipChildren - Don't query integrations for sub-indicators
@param {string[]} tags - Tags applied at the time of search
@param {string | undefined} viewId - The ID of the view at the time of search (if any)
@returns {IntegrationChunk[]} results - An array data chunks with the data