Search Ohio Secretary of State Business Records

Site businesssearch.ohiosos.govTask ohio-business-searchVersion v6Updated Aug 9, 2026Category browser-automation

Search Ohio SOS business records through the site's jQuery API or protected form, clearing Cloudflare before access and extracting structured results. This skill was captured from a live agent session on businesssearch.ohiosos.gov and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Search the Ohio Secretary of State business database by a supplied business-name query and return structured matching business records. Prefer the site's jQuery JSON API for a direct extraction path; use the protected search form as a fallback when the API or endpoint configuration is unavailable.

When to Use

Use for any Ohio business-name search where the caller provides {query} and needs matching business records, either as raw API records or normalized records from the rendered result table.

Workflow

  1. Navigate to https://businesssearch.ohiosos.gov/ with waitUntil: "load" and a timeout of about 45 seconds.
  2. If Cloudflare protection is presented or page readiness is delayed, run solve(cloudflare) with a timeout of about 30 seconds. After the solver reports success, navigate to https://businesssearch.ohiosos.gov/ again with waitUntil: "domcontentloaded" before probing the page or API; the post-solve navigation establishes the usable session state.
  3. On the post-solve page, wait inside one evaluate() call until jQuery is available, then load the endpoint configuration and call the business-name endpoint directly. The endpoint is discovered from ajax/endPoints.json; do not hard-code or guess an opaque API route:
(async () => {
  const query = { query };
  const waitUntil = async (test, timeout = 40000) => {
    const started = Date.now();
    while (!test()) {
      if (Date.now() - started > timeout)
        throw new Error("Timed out waiting for Ohio SOS page readiness");
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
  };
  const result = { searchQuery: query, url: location.href, title: document.title };
  try {
    await waitUntil(
      () => typeof jQuery !== "undefined" && location.href !== "about:blank",
    );
    const endpointConfig = await jQuery.getJSON("ajax/endPoints.json");
    const endpoint = endpointConfig?.data?.[0]?.businessnamediv;
    if (!endpoint) throw new Error("businessnamediv endpoint was not provided");
    const base = endpoint.endsWith("/") ? endpoint : endpoint + "/";
    const searchUrl = base + "NS_" + encodeURIComponent(query) + "_X";
    result.searchUrl = searchUrl;
    const payload = await new Promise((resolve, reject) => {
      jQuery.ajax({
        url: searchUrl,
        xhrFields: { withCredentials: true },
        type: "GET",
        crossDomain: true,
        cache: false,
        dataType: "json",
        timeout: 25000,
        success: resolve,
        error: (xhr, status, error) =>
          reject({
            status: xhr.status,
            statusText: status,
            error,
            responseText: (xhr.responseText || "").slice(0, 500),
          }),
      });
    });
    const records = Array.isArray(payload?.data) ? payload.data : [];
    return {
      ...result,
      status: "success",
      resultCount: records.length,
      records,
      responseKeys: Object.keys(payload || {}),
    };
  } catch (error) {
    return {
      ...result,
      status: "error",
      error: error?.message || error?.statusText || String(error),
      errorDetail: error,
    };
  }
})();
  1. If the API path fails because Cloudflare has not cleared or the endpoint configuration is unavailable, wait up to 40 seconds for input#bSearch, submit {query} through the form, wait about 8–10 seconds, and extract the table in the same evaluate() call:
(async () => {
  const input = document.querySelector("input#bSearch");
  if (!input)
    return {
      error: "Search input not found",
      url: location.href,
      title: document.title,
    };
  input.value = { query };
  input.dispatchEvent(new Event("input", { bubbles: true }));
  input.dispatchEvent(new Event("change", { bubbles: true }));
  const button =
    document.querySelector(
      "input#h1-onload ~ div input[type='button'][value='SEARCH']",
    ) || document.querySelector('input[value="SEARCH"]');
  if (!button)
    return {
      error: "Search button not found",
      searchQuery: input.value,
      url: location.href,
      title: document.title,
    };
  button.click();
  await new Promise((resolve) => setTimeout(resolve, 10000));
  const results = [];
  for (const row of document.querySelectorAll("table tr")) {
    const cells = row.querySelectorAll("td");
    if (cells.length >= 6)
      results.push({
        entityId: cells[0]?.innerText?.trim() || "",
        name: cells[1]?.innerText?.trim() || "",
        type: cells[2]?.innerText?.trim() || "",
        filingDate: cells[3]?.innerText?.trim() || "",
        expDate: cells[4]?.innerText?.trim() || "",
        status: cells[5]?.innerText?.trim() || "",
        state: cells.length > 8 ? cells[8]?.innerText?.trim() || "" : "",
      });
  }
  return {
    searchQuery: input.value,
    resultCount: results.length,
    results,
    url: location.href,
    title: document.title,
    bodySnippet: results.length
      ? ""
      : (document.body?.innerText || "").slice(0, 1000),
  };
})();

Site-Specific Gotchas

  • Cloudflare protection may require an explicit solver immediately after the initial navigation. After solve(cloudflare) succeeds, perform a second navigation to the site before accessing jQuery, the API, or the search form.
  • The durable API route is discovered from ajax/endPoints.json, specifically data[0].businessnamediv.
  • Business-name API requests use the non-obvious NS_{URL-encoded-query}_X path format. Preserve the NS_ prefix and _X suffix and URL-encode the query rather than guessing an entity URL.
  • The API is queried with jQuery using crossDomain: true, xhrFields: { withCredentials: true }, dataType: 'json', and cache disabled.
  • Cloudflare may temporarily block or delay access; the API evaluator should wait for jQuery and a non-blank page before requesting the endpoint. For form fallback, do not type or click until input#bSearch exists.
  • The preferred form search-button selector is input#h1-onload ~ div input[type='button'][value='SEARCH']; fall back to the first input[value='SEARCH'] only if the preferred selector is absent.
  • API results are in payload.data; form result rows are table tr elements with at least six td cells. In the form table, cells 0–5 map to entity ID, name, type, filing date, expiration date, and status; cell 8 may contain state.
  • An empty form result list can mean that the page is still loading or the table structure changed; use bodySnippet for diagnosis rather than treating it automatically as proof of no matches.

Expected Output

Prefer an object containing searchQuery, the discovered searchUrl, status, resultCount, raw API records, and response keys. If the API path is unavailable, return the fallback form object containing the submitted query, resultCount, normalized business records, current URL and title, and a short body snippet when no qualifying rows are present.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=businesssearch.ohiosos.gov&task=ohio-business-search