---
title: "Identify a merchant"
method: POST
path: "/transaction"
tags: ["Transaction Enrichment API"]
---

# Identify a merchant

`POST /transaction`

## Request

- Base URL: `https://www.logo.dev/api`
- URL: `https://www.logo.dev/api/transaction`
- Auth: HTTP bearer

Identify the merchant behind a merchant name or card descriptor. Every non-empty `transaction` returns one row: `matched` with a merchant domain, or `cleaned` with a display name and no domain.

Transaction is a private beta with its own host and its own `tx_beta_` key. Errors use a single `error` string, not the Logo.dev API error object. Read the [Transaction guide](/transaction/introduction) for access, the daily allowance, and how to act on a row. Descriptor matching is experimental.

## Request body

- object: The body takes no fields other than these. An unrecognized field returns `400`.
  - `transaction` string, required: The merchant name or card descriptor. The API trims surrounding whitespace, then the value must be 1 to 1,024 characters.
  - `country_code` string: ISO 3166-1 alpha-2 country code, such as `US` or `GB`. Lowercase is accepted. The code is a hint for ambiguous names. It does not restrict results to that country. An unknown code returns `400`. With `transaction_context`, the codes `AQ`, `AX`, `TF`, and `VA` return `400`.
  - `transaction_context` object: The real transaction values. Send them to identify the other party as a person or an organization, and to name the platform the purchase ran through. With context, the response adds `entity_type`. When you send the object, `date`, `amount`, `currency`, and `entry_type` are required. The API fills in no placeholder values. Incomplete context returns `400` and does not count against the daily allowance. The merchant string, the context, and the account holder details go to Logo.dev's enrichment provider. Each lookup carries its own context. The beta keeps no account history across lookups.
    - `date` string, date, required: The posted date as `YYYY-MM-DD`, from `1970-01-01` to tomorrow in UTC.
    - `amount` number, required: A nonnegative amount in the currency's major units, such as dollars, up to 1,000,000,000,000.
    - `currency` string, required: An ISO 4217 code that the beta supports. Lowercase is accepted. An unsupported code returns `400`. Supported codes: EUR, AED, AFN, XCD, ALL, AMD, AOA, ARS, USD, AUD, AWG, AZN, BAM, BBD, BDT, XOF, BGN, BHD, BIF, BMD, BND, BOB, BRL, BSD, INR, NOK, BWP, BYR, BZD, CAD, CDF, XAF, CHF, NZD, CLP, CNY, COP, CRC, CUP, CVE, ANG, CZK, DJF, DKK, DOP, DZD, EGP, MAD, ERN, ETB, FJD, FKP, GBP, GEL, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HUF, IDR, ILS, IQD, IRR, ISK, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, ZAR, LYD, MDL, MGA, MKD, MMK, MNT, MOP, MRO, MUR, MVR, MWK, MXN, MYR, MZN, XPF, NGN, NIO, NPR, OMR, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLL, SOS, SRD, SSP, STD, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, UYU, UZS, VEF, VND, VUV, WST, YER, ZMW, ZWL, HRK.
    - `entry_type` 'incoming' | 'outgoing', required: The direction of the money, from the bank account owner's side.
    - `account_holder` object: The bank account owner. Send it when the account holder matters, for example to tell a person from a business.
      - `type` 'consumer' | 'business', required: Whether the bank account owner is a consumer or a business.
      - `name` string: The bank account owner's name. The API trims surrounding whitespace. Send it when a descriptor holds several names and the owner's name helps pick out the other party.

## Response `200`

A `matched` row or a `cleaned` row. Every non-empty `transaction` returns one. The examples are illustrative: they show the shape, not the result for a particular string.

- object
  - `domain` string, nullable, required: The merchant's domain. `null` on a `cleaned` row.
  - `name` string, required: The display name. Present for every non-empty `transaction`. - When Brand resolves a brand for the domain, its name wins. - Otherwise it is the name the transaction lookup returned. - On a `cleaned` row it is the descriptor, tidied into a display name. Descriptors arrive in one case, so a tidied name reads `Fedex`, not `FedEx`. When the Logo.dev brand corpus holds the same name, a `cleaned` row takes the company's own spelling. Only the name changes. The row stays `cleaned` at the same `confidence`.
  - `entity_type` 'person' | 'organization' | 'null', nullable: Present only when the request includes `transaction_context`. `person` or `organization`, as the enrichment provider classified the other party, or `null` when it classified nobody. A `null` does not mean the API found no merchant. A later stage can identify one, so a `matched` row can carry `null` here. A person always comes back as a `cleaned` row. Without context, the API does not detect people. It treats a personal name as a merchant name, so `Jane Doe` can match an organization with a similar name.
  - `entity_id` string, nullable, required: The merchant's Logo.dev brand id, the same id Brand uses. A brand keeps its id when its domain changes, so key your records on `entity_id`, not `domain`. A row with an id has two sources that agree: the pipeline identified the merchant, and Brand resolves a brand for the same domain. Branch on `entity_id`, not on `confidence`. `null` on a `cleaned` row, and on a `matched` row when Brand resolves no brand for the domain. A domain that Logo.dev has not indexed yet also returns `null`. A later lookup can return an id after the first crawl finishes.
  - `logo_url` string, uri, nullable, required: The Logo.dev CDN URL for the merchant's logo. `null` on a `cleaned` row. The URL has no token. Add your publishable key before you show it: `https://img.logo.dev/starbucks.com?token=LOGO_DEV_PUBLISHABLE_KEY`. The same applies to `logo_url` in `parent` and `intermediary`.
  - `parent` object, nullable, required: The company that owns the merchant, or `null`. It sits beside the merchant and never replaces it: a lookup for Facebook returns Facebook in `domain`, with Meta here. Group totals on `parent.entity_id` to add up one company's brands. Facebook, Instagram, and WhatsApp become Meta. The owner's `entity_id` is the same id it has when it is the merchant, so the two join on one column. `parent` is the nearest owner that Logo.dev records, not the top of the chain. Jaguar returns JLR, and JLR returns Tata Motors. To find the top, look up each parent in turn. Coverage is thin. Logo.dev records a small, hand-verified list of ownership pairs, so most rows return `null`. A `null` means Logo.dev records no owner for this merchant. It does not mean the merchant is independent. `parent` is also `null` on a `cleaned` row, and when Brand resolves no brand for the owner's domain. A rebrand onto a new domain can come back as a new identity. `parent` joins it to the old one only when both domains are in the ownership list.
    - `domain` string, nullable, required: The owner's domain.
    - `name` string, nullable, required: The owner's display name, from Brand.
    - `entity_id` string, nullable, required: The owner's Logo.dev brand id, the same id Brand uses.
    - `logo_url` string, nullable, required: The Logo.dev CDN URL for the owner's logo, with no token.
  - `intermediary` object, nullable, required: The platform the purchase ran through, or `null`. The merchant stays in `domain`. A coffee paid through Square returns the coffee shop, with Square here. The platform gets the same lookup as the merchant, keyed by its own domain, so you can group totals by platform on `intermediary.entity_id`. `intermediary` is `null` in these cases: - The platform sold the item itself. A DashPass subscription is a purchase from DoorDash, so that row names DoorDash in `domain`. - The API identified no platform. This does not mean no platform was involved. Without `transaction_context`, the API fills `intermediary` only when the enrichment provider answers the whole descriptor with a platform. The merchant checks then refuse the platform as the seller, and the API looks up the text after the prefix on its own. When the provider resolves the seller directly from a prefixed descriptor, `intermediary` stays `null` on a correct row. Send `transaction_context` when you need the platform named reliably. A `cleaned` row can keep a platform. This happens when the checks reject the platform's domain and the text after the prefix identifies nobody.
    - `domain` string, nullable, required: The platform's domain, or `null`.
    - `name` string, nullable, required: The platform's display name, or `null`. Brand's name wins when there is one.
    - `entity_id` string, nullable, required: The platform's Logo.dev brand id, or `null` when Brand resolves no brand for its domain.
    - `logo_url` string, nullable, required: The Logo.dev CDN URL for the platform's logo, with no token, or `null` when it has no domain.
  - `status` 'matched' | 'cleaned', required: `matched` when the API identified a merchant domain. `cleaned` when it did not. There is no not-found result: a `cleaned` row is an answer, with a tidied display name and `null` in `domain`, `entity_id`, `logo_url`, and `parent`. A row is `cleaned` for one of two reasons: - Nothing identified the descriptor. - A candidate domain failed a merchant check. The checks catch repeated hostnames, directory and profile hosts, a known platform in the merchant's place, and known descriptor conflicts. They run on saved results and on new lookups. A failed check drops the domain and uses the tidied descriptor as the name. A platform that a check removes from the merchant's place still comes back in `intermediary`. An organization with no usable website also comes back `cleaned`. The API never puts a payment processor's or parent company's domain in place of the merchant's.
  - `match_type` 'enriched_context' | 'descriptor_domain' | 'resolved_entity' | 'searched_name' | 'cleaned_descriptor', required: The pipeline stage that produced the row, strongest first. The set is closed. - `enriched_context`: identified from the full transaction, with entry type and account holder. - `descriptor_domain`: the descriptor printed the merchant's own domain. - `resolved_entity`: identified from the descriptor alone, with no transaction context. - `searched_name`: the Logo.dev brand corpus recognized the cleaned name. - `cleaned_descriptor`: nothing identified the merchant. `name` holds the tidied descriptor. `descriptor_domain` and `searched_name` are fallbacks. They run only when the enrichment provider identifies nobody. A descriptor that the provider recognizes never reaches them: `CHEWY.COM XXX-XXX-7111 FL` prints a domain and still returns `resolved_entity`. Both fallbacks face the same merchant checks, so a row that fails them comes back `cleaned`. Read `match_type` from the row. Do not predict it from the descriptor, because the same string can take a different path as the provider's coverage changes.
  - `confidence` number, required: A score from 0 to 1. Two inputs set it: the `match_type` that produced the row, and whether Brand resolves a brand for the same domain. A printed domain scores above a name resolved from the descriptor, and a corpus search scores below it. A failed merchant check sends the row to the floor. A `matched` row with no brand has a lower score and a `null` `entity_id`. The same input always gets the same score, so two runs of a batch compare cleanly. Logo.dev publishes no cut-off, because the right line depends on what the row feeds. Use the score to rank and triage, for example to sort a review queue. To branch, read `entity_id`. Logo.dev can recalibrate the score without changing which rows have an id. A high score does not guarantee accuracy.

## Other responses

- `400`: The request failed validation: invalid JSON, an empty or oversized `transaction`, an unknown `country_code`, invalid or incomplete `transaction_context`, or an unknown field. It does not count against the daily allowance.
- `401`: The `Authorization` header is missing, or the beta key is malformed, unknown, or replaced by a newer key.
- `403`: The beta is not on for this account. Access belongs to a Logo.dev account, so this applies to everyone on it.
- `409`: Another request is already looking up the same transaction. Wait for `Retry-After` and send the request again to read the saved result. The original request, the `409`, and the retry all count against the daily allowance.
- `413`: The request body is larger than 8,192 bytes.
- `415`: The `Content-Type` is not `application/json`.
- `429`: A rate limit applies. Wait for `Retry-After` before you send the request again. - The daily allowance is spent. The response has the usage headers, and `Retry-After` counts the seconds to the next midnight UTC. - More than 60 requests in one minute. The response has `Retry-After` and no usage headers.
- `503`: Enrichment, storage, or the access check is unavailable. The API returns this error, not a `cleaned` row. A failed lookup still counts against the daily allowance.

---

[API](https://skmtc.dev/logo/apis/logo-dev-api.md) · [All operations](https://skmtc.dev/logo/apis/logo-dev-api/llms.txt) · [OpenAPI document](https://skmtc.dev/logo/apis/logo-dev-api/revisions/6251e2aca8fb?raw)
