Docs

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.

Authentication

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.

POST /v1/clean

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.

Example request

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"}'

Example response

{
  "input": "POS PURCHASE SQ *MAPLE STREET COFFEE 1234 OAKLAND CA",
  "merchant": "Maple Street Coffee",
  "category": "meals",
  "confidence": 0.6,
  "match": "keyword",
  "processor": "Square"
}

Batch body

{"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.

Response fields

FieldMeaning
merchantCleaned name, or null if nothing usable was left
categoryOne 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
confidence0 to 1. A rough guide to how much evidence we had, not a measured accuracy rate
matchmerchant_list, keyword, mcc, processor_hint, cleaned_text, or none (how the result was reached)
processorPayment processor prefix seen in the text (for example Square), or null
mcc, mcc_descriptionEchoed when you send an MCC. Descriptions come from the public-domain greggles/mcc-codes list

Other endpoints

Limits

Plans differ by speed and batch size. There is no monthly call count.

Plan/v1/clean requests per minuteDescriptors per requestHoliday requests per minute
Free10105
Starter1201005
Scale6001005
Holidays Basic key1010120

Business Days & Holidays add-on

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.

Coverage gaps

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.

CountryDefaultGaps
USnationalFederal holidays only by default; state holidays (e.g. Cesar Chavez Day, Patriots Day) need subdiv. Federal observance does not mean banks or businesses close.
CAnationalFederal statutory holidays only by default; provincial holidays (Family Day, Civic Holiday, Saint-Jean-Baptiste, etc.) need subdiv.
GBENGDefaults to England and Wales (ENG). Scotland (SCT) and Northern Ireland (NIR) bank holidays differ; pass subdiv.
AUnationalNational holidays only by default; state holidays such as the King's Birthday (dates vary by state), Labour Day and Melbourne Cup need subdiv.
NZnationalRegional anniversary days need subdiv.
DEnationalLander holidays (e.g. Epiphany, Corpus Christi, Reformation Day) need subdiv.
FRnationalAlsace-Moselle and overseas holidays need subdiv.
ESnationalRegional holidays need subdiv; local holidays are not covered.
ITnationalProvincial patron saint days need subdiv.
BEnationalCommunity holidays need subdiv.
ATnationalState patron days need subdiv.
CHnationalVery few nationwide holidays; most are cantonal, so pass subdiv.
PTnationalRegional and municipal holidays not fully covered.
JPnationalNo subdivisions available.
MXnationalOnly official federal rest days; no subdivisions available.
BRnationalState 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

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

Errors look like {"error": {"code": "rate_limited", "message": "..."}}.

StatuscodeWhen
400bad_json / bad_request / empty_batch / bad_emailThe body is not valid JSON or is missing a descriptor.
401missing_key / invalid_keyNo key, a malformed or altered key, or an expired free key.
402key_expired / subscription_inactiveA paid key past its expiry date, or a refresh for a subscription that is no longer active. Renew, then POST /v1/refresh.
405method_not_allowedWrong HTTP method.
409already_issuedA free key already exists for that email.
413batch_too_large / body_too_largeMore descriptors than your plan allows per request, or a body over 64 KB.
429rate_limited / demo_limit / signup_limitToo many requests this minute for your key (see Retry-After), or the keyless demo or signup limit for your IP today.
500internalOur bug. Email us if it repeats.

Data

We do not store descriptors you send. We do not store your key: it is signed, and we check the signature on each call.