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
- Navigate to
https://businesssearch.ohiosos.gov/withwaitUntil: "load"and a timeout of about 45 seconds. - 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 tohttps://businesssearch.ohiosos.gov/again withwaitUntil: "domcontentloaded"before probing the page or API; the post-solve navigation establishes the usable session state. - 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 fromajax/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,
};
}
})();- 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 sameevaluate()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, specificallydata[0].businessnamediv. - Business-name API requests use the non-obvious
NS_{URL-encoded-query}_Xpath format. Preserve theNS_prefix and_Xsuffix 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#bSearchexists. - The preferred form search-button selector is
input#h1-onload ~ div input[type='button'][value='SEARCH']; fall back to the firstinput[value='SEARCH']only if the preferred selector is absent. - API results are in
payload.data; form result rows aretable trelements with at least sixtdcells. 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
bodySnippetfor 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.