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.
Use Cases
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.
Automation Flow
- 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),
};
})();Possible Friction Points
- 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.