One endpoint: send raw transaction descriptors, get back a cleaned merchant name, one of our categories, and a confidence score from 0 to 1. Results are automated suggestions. They can be wrong, especially for small or local merchants, so check anything that matters (taxes, bookkeeping) yourself.
Header: Authorization: Bearer <your key>. X-API-Key: <your key> also works. Keys start with mcl_. Get a free key at /signup. Keep keys server-side.
Single: {"descriptor": "...", "mcc": "5411", "amount": -12.5} (mcc and amount optional). Batch: {"descriptors": [ ... ]} with up to 10 items on Free and 100 on Starter and Scale; each item is a string or an object like the single form.
curl -X POST https://merchantcleanup.pages.dev/v1/clean \
-H "Authorization: Bearer mcl_your_key" \
-H "Content-Type: application/json" \
-d '{"descriptor": "POS PURCHASE SQ *MAPLE STREET COFFEE 1234 OAKLAND CA"}'{
"input": "POS PURCHASE SQ *MAPLE STREET COFFEE 1234 OAKLAND CA",
"merchant": "Maple Street Coffee",
"category": "meals",
"confidence": 0.6,
"match": "keyword",
"processor": "Square"
}{"descriptors": [
"TST* JOES DINER #0042 AUSTIN TX",
{"descriptor": "MAPLE STREET MARKET 0091", "mcc": "5411", "amount": -42.10}
]}
Batch responses are {"results": [...], "count": N} in input order. An item that cannot be read returns {"input": ..., "error": "..."} in its slot and is still returned.
| Field | Meaning |
|---|---|
| merchant | Cleaned name, or null if nothing usable was left |
| category | One of: income, transfer, groceries, meals, travel, auto_and_transport, shopping, software_and_subscriptions, office_and_supplies, utilities_and_phone, insurance, rent_and_housing, professional_services, contractors, advertising, fees_and_interest, taxes, payroll, health, entertainment, charity, education, other |
| confidence | 0 to 1. A rough guide to how much evidence we had, not a measured accuracy rate |
| match | merchant_list, keyword, mcc, processor_hint, cleaned_text, or none (how the result was reached) |
| processor | Payment processor prefix seen in the text (for example Square), or null |
| mcc, mcc_description | Echoed when you send an MCC. Descriptions come from the public-domain greggles/mcc-codes list |
GET /v1/demo?q=... - keyless demo, one descriptor per request, limited per IP per dayGET /v1/categories - category listPOST /v1/refresh (with your paid key) - checks your subscription with Stripe and returns a new key with a fresh expiry (up to 35 days). Inactive subscription: 402Plans differ by speed and batch size. There is no monthly call count.
| Plan | /v1/clean requests per minute | Descriptors per request | Holiday requests per minute |
|---|---|---|---|
| Free | 10 | 10 | 5 |
| Starter | 120 | 100 | 5 |
| Scale | 600 | 100 | 5 |
| Holidays Basic key | 10 | 10 | 120 |
rate_limited with a Retry-After header. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining.POST /v1/refresh while your subscription is active; after a cancel, a key stops working when it expires.National public holidays for 26 countries (US, CA, GB, IE, AU, NZ, DE, FR, ES, IT, NL, BE, AT, CH, SE, NO, DK, FI, PL, PT, CZ, JP, SG, MX, BR, ZA), years 2025 to 2028. Weekend is Saturday and Sunday for every country. For GB, a holiday that falls on a weekend is listed only on its substitute weekday, named "(substitute day)", as on gov.uk; other countries may list both the actual date and an "(observed)" date. Add subdiv (a state, province or region code, for example subdiv=CA for California or subdiv=SCT for Scotland) to any endpoint to include that region's holidays; codes are listed at /v1/holidays/countries. Without subdiv you get nationwide holidays only, except GB, which defaults to England and Wales (ENG). Data is generated from the open-source python-holidays project (MIT licence, notice). Dates can be wrong or change; check anything that matters.
Same key and header as /v1/clean. Holiday endpoints have their own per-minute limit per key: 5 requests a minute without the add-on, 120 with Holidays Basic ($5/month, pricing). A Holidays Basic purchase issues its own key; on it, /v1/clean uses Free plan limits. Holidays Pro is coming soon. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining.
GET /v1/holidays?country=US&year=2026 - list of {date, name}GET /v1/is-business-day?country=GB&date=2026-12-28 - business_day, weekend, holiday (name or null)GET /v1/business-days?country=DE&start=2026-12-01&end=2026-12-31 - inclusive counts, up to 731 daysGET /v1/add-business-days?country=US&date=2026-11-25&days=3 - moves forward (or back with a negative number, up to 1000) skipping weekends and holidaysGET /v1/holidays/countries - countries, subdivision codes, defaults and coverage gaps (no key, not counted)Without subdiv, only nationwide public holidays are returned. Not covered anywhere: municipal or city holidays, bank-only or school holidays, half days, and one-off holidays declared after the data was built.
| Country | Default | Gaps |
|---|---|---|
| US | national | Federal holidays only by default; state holidays (e.g. Cesar Chavez Day, Patriots Day) need subdiv. Federal observance does not mean banks or businesses close. |
| CA | national | Federal statutory holidays only by default; provincial holidays (Family Day, Civic Holiday, Saint-Jean-Baptiste, etc.) need subdiv. |
| GB | ENG | Defaults to England and Wales (ENG). Scotland (SCT) and Northern Ireland (NIR) bank holidays differ; pass subdiv. |
| AU | national | National holidays only by default; state holidays such as the King's Birthday (dates vary by state), Labour Day and Melbourne Cup need subdiv. |
| NZ | national | Regional anniversary days need subdiv. |
| DE | national | Lander holidays (e.g. Epiphany, Corpus Christi, Reformation Day) need subdiv. |
| FR | national | Alsace-Moselle and overseas holidays need subdiv. |
| ES | national | Regional holidays need subdiv; local holidays are not covered. |
| IT | national | Provincial patron saint days need subdiv. |
| BE | national | Community holidays need subdiv. |
| AT | national | State patron days need subdiv. |
| CH | national | Very few nationwide holidays; most are cantonal, so pass subdiv. |
| PT | national | Regional and municipal holidays not fully covered. |
| JP | national | No subdivisions available. |
| MX | national | Only official federal rest days; no subdivisions available. |
| BR | national | State holidays need subdiv; municipal holidays are not covered. |
curl "https://merchantcleanup.pages.dev/v1/add-business-days?country=US&date=2026-11-25&days=3" -H "Authorization: Bearer mcl_your_key"
{"country":"US","date":"2026-11-25","days":3,"result":"2026-12-01"}
Errors: 400 bad_country / bad_subdiv / bad_year / bad_date / bad_range / bad_days / out_of_range; 429 rate_limited.
CORS is enabled for all origins (Access-Control-Allow-Origin: *, methods GET, POST, OPTIONS). That lets you try the API from a browser, but do not put a key in public front-end code.
Errors look like {"error": {"code": "rate_limited", "message": "..."}}.
| Status | code | When |
|---|---|---|
| 400 | bad_json / bad_request / empty_batch / bad_email | The body is not valid JSON or is missing a descriptor. |
| 401 | missing_key / invalid_key | No key, a malformed or altered key, or an expired free key. |
| 402 | key_expired / subscription_inactive | A paid key past its expiry date, or a refresh for a subscription that is no longer active. Renew, then POST /v1/refresh. |
| 405 | method_not_allowed | Wrong HTTP method. |
| 409 | already_issued | A free key already exists for that email. |
| 413 | batch_too_large / body_too_large | More descriptors than your plan allows per request, or a body over 64 KB. |
| 429 | rate_limited / demo_limit / signup_limit | Too many requests this minute for your key (see Retry-After), or the keyless demo or signup limit for your IP today. |
| 500 | internal | Our bug. Email us if it repeats. |
We do not store descriptors you send. We do not store your key: it is signed, and we check the signature on each call.