@radsoft/markbase
v2026.8.3
Published
Official Node client for the Markbase USPTO trademark API — search, screen, and look up 14M+ US trademarks.
Maintainers
Readme
@radsoft/markbase
Node client for the Markbase USPTO trademark API — search, screen, and look up 14M+ US trademarks. Fully typed, zero dependencies.
npm i @radsoft/markbaseCovers the USPTO only. If you need EUIPO, WIPO, or UKIPO, a multi-office platform is a better fit — we'd rather say so than waste your time.
30 seconds
No key needed to try it. The keyless tier allows 1,000 requests a month; a free account raises that to 5,000.
import { Markbase } from "@radsoft/markbase";
const mb = new Markbase(); // reads MARKBASE_API_KEY if set
const { highest_risk, matches } = await mb.clearance("nikee", {
trademarkClass: 25, // clothing
});
console.log(highest_risk); // "high"
console.log(matches[0].word_mark); // "NIKE"
console.log(matches[0].owner_name);// "Nike, Inc."
console.log(matches[0].similarity);// 0.85The one thing to get right: classes
Trademark rights are scoped to goods and services, not to names. DELTA coexists as an airline, a faucet maker, and dental insurance — same word, three owners, no conflict.
So screening without a class gives you noise:
await mb.clearance("Delta"); // ~everything, mostly irrelevant
await mb.clearance("Delta", { trademarkClass: 9 }); // only what could block youThere are 45 classes. Fetch them to build a picker:
const classes = await mb.classes();
// [{ code: "009", title: "Computers & electronics" }, ...]Common ones: 9 software · 25 clothing · 35 advertising & business · 41 education & entertainment · 42 science & technology services (where most SaaS sits).
Recipes
Screen names from a generator
The usual shape: generate candidates, drop the ones that are obviously taken.
import { Markbase } from "@radsoft/markbase";
const mb = new Markbase();
async function viable(names: string[], trademarkClass: number) {
const checks = await Promise.all(
names.map(async (name) => {
const { highest_risk } = await mb.clearance(name, { trademarkClass });
return { name, risk: highest_risk };
})
);
return checks.filter((c) => c.risk === "low" || c.risk === "none");
}
await viable(["Zeplo", "Nikee", "Flowmint"], 25);
// [{ name: "Zeplo", risk: "low" }, { name: "Flowmint", risk: "low" }]
// "Nikee" is dropped — high risk against NIKE in class 25Flag conflicts at checkout
For a registrar or a "claim your brand" flow — warn before they pay.
const { highest_risk, matches } = await mb.clearance(brand, {
trademarkClass: 42,
limit: 3,
});
if (highest_risk === "high") {
return {
warn: `“${matches[0].word_mark}” is already registered by ${matches[0].owner_name}.`,
conflicts: matches,
};
}Watch a competitor's portfolio
const { hits } = await mb.byOwner("Nike, Inc.", { limit: 50 });
const live = hits.filter((m) => statusOf(m.status_code) === "registered");
console.log(`${live.length} live in this page`);
// note: total_hits saturates at 1000 for large portfolios — page through with
// the `page` option, or use aggregate() if you need a true countAnalyse a whole class
total_hits caps at 1,000, so use aggregate() when you want real numbers.
const { buckets } = await mb.aggregate("class");
// [{ key: "009", count: 1839467 }, { key: "035", count: 1437003 }, ...]
const byCountry = await mb.aggregate("country");
const byYear = await mb.aggregate("year");Walk a large result set
paginate() handles the page arithmetic and stops when results run out.
for await (const mark of mb.paginate({ q: "coffee", limit: 100 }, 5)) {
console.log(mark.serial_number, mark.word_mark);
}In a Next.js route handler
// app/api/check/route.ts
import { Markbase } from "@radsoft/markbase";
const mb = new Markbase({ apiKey: process.env.MARKBASE_API_KEY });
export async function POST(req: Request) {
const { name, trademarkClass } = await req.json();
const result = await mb.clearance(name, { trademarkClass });
return Response.json(result);
}Keep the key server-side. It's a secret; don't ship it to the browser.
Reference
| Method | Returns |
|---|---|
| search(opts \| string) | SearchResult — typo-tolerant full-text search |
| clearance(mark, opts?) | ClearanceResult — ranked conflicts with a risk level |
| suggest(q, limit?) | Suggestion[] — autocomplete, tuned for as-you-type |
| getTrademark(serial) | Trademark — one full record |
| getTimeline(serial) | prosecution history |
| byOwner(name, opts?) | OwnerResult — an owner's portfolio |
| getBatch(serials) | BatchResult — up to 50 at once (Scale plan) |
| aggregate(field) | AggregateResult — counts by class/status/country/drawing/year |
| classes() | NiceClass[] — all 45 |
| info() | IndexInfo — index size and freshness |
| paginate(opts, maxPages?) | AsyncGenerator<Trademark> |
Search options
await mb.search({
q: "acme", // typo-tolerant: "gogle" finds Google
trademarkClass: 9, // 9, "009", or [9, 42] all work
statusCode: "800", // 800 = registered
ownerCountry: "US",
ownerState: "CA",
filingDateFrom: "20200101", // YYYYMMDD
filingDateTo: "20251231",
sort: "filing_date:desc",
page: 1,
limit: 50,
facets: true, // include distribution counts
});Helpers
USPTO records are raw. These make them presentable:
import { statusOf, parseDate, padClass } from "@radsoft/markbase";
statusOf("800"); // "registered"
statusOf("601"); // "dead"
statusOf("710"); // "cancelled"
parseDate("19720131"); // Date(1972-01-31) — the register stores YYYYMMDD
padClass(9); // "009"Errors
Typed, so you can branch on what actually went wrong rather than parsing strings.
import {
MarkbaseError, AuthError, PlanError, RateLimitError, NotFoundError,
} from "@radsoft/markbase";
try {
await mb.getBatch(serials);
} catch (err) {
if (err instanceof PlanError) {
// endpoint isn't on this plan — batch is Scale-only
} else if (err instanceof RateLimitError && err.kind === "quota") {
// month's allowance is gone; err.resetsAt says when it returns
} else if (err instanceof AuthError) {
// bad or missing key
} else if (err instanceof NotFoundError) {
// no such serial number
}
}| Error | When |
|---|---|
| AuthError | 401 — key missing or invalid |
| PlanError | 403 — endpoint not on your plan |
| NotFoundError | 404 — no such record |
| RateLimitError | 429 — kind is "rate" or "quota" |
| MarkbaseError | anything else, including network failures (status: 0) |
Rate limits and quota
Per-minute limits and 5xx are retried automatically with backoff
(maxRetries, default 2).
Monthly quota exhaustion is not retried — waiting a few seconds can't fix a
month-long window, so it throws immediately with resetsAt rather than burning
your retries on a call that can't succeed.
Live limits sit on the client after any call:
await mb.search({ q: "nike" });
mb.rateLimit;
// {
// plan: "launch",
// rateLimit: 120, rateRemaining: 119,
// quotaLimit: 50000, quotaRemaining: 49873,
// quotaResetsAt: Date
// }Useful for backing off before you hit the wall:
if (mb.rateLimit && mb.rateLimit.quotaRemaining !== null && mb.rateLimit.quotaRemaining < 100) {
console.warn("Nearly out of quota this month");
}Options
new Markbase({
apiKey: process.env.MARKBASE_API_KEY, // null forces the keyless tier
baseUrl: "https://api.markbase.co",
timeout: 30_000, // ms before abort
maxRetries: 2, // rate limits and 5xx
fetch: customFetch, // inject your own
});
new Markbase("mb_your_key"); // shorthandapiKey falls back to MARKBASE_API_KEY. Pass null to force keyless even
when that variable is set.
TypeScript
Ships with declarations — no @types package needed.
import type {
Trademark, SearchResult, ClearanceResult, ClearanceMatch,
MarkStatus, RiskLevel, NiceClass, RateLimitInfo,
} from "@radsoft/markbase";
function render(m: ClearanceMatch): string {
return `${m.word_mark} — ${m.risk} risk (${m.similarity})`;
}Not legal advice
Clearance returns a first-pass screening signal over the federal register. It does not cover state trademarks, common-law rights, or the legal analysis of likelihood of confusion — no API does.
Treat a clean result as worth pursuing, not cleared, and put a trademark attorney at the end of the funnel for any name you actually intend to adopt.
Also available
- @radsoft/markbase-mcp — MCP server, so Claude and other assistants can screen names directly
- API docs · OpenAPI spec · Trademark classes
License
MIT — a product of Rad Soft.
