Overview
Tesouro flags payables that look like duplicates of each other. It never cancels a payable on its own: an organization user reviews each flagged pair and decides whether to keep both or cancel the copy. Two payables of the same organization are considered possible duplicates when they have the same invoice number (document_id) and the same vendor. Tesouro compares vendors in one of two ways:
- If both payables are linked to a counterpart, they must have the same
counterpart_id. - If at least one of them isn’t linked to a counterpart yet, Tesouro compares the vendor name that OCR read from the invoice (
counterpart_raw_data.name). The comparison ignores case, punctuation, accents, and trailing legal suffixes such as Inc, LLC, or Ltd, so “Acme Inc.” and “ACME” match.
When detection runs
Detection runs automatically, and its result is already included in the response of the request that triggered it. There is no delay and no separate job to wait for. Tesouro checks for duplicates when:- a payable is created from data;
- OCR finishes processing an uploaded or emailed invoice;
- a payable is updated and its
counterpart_idordocument_idchanges; - a counterpart is linked automatically while the payable moves through its lifecycle, for example on submission for approval.
The duplicates array
Every payable returned by the API, whether in a list, on its own, or in the response to an update or a status change, includes a duplicates array. It is empty when there is nothing to report:
A few things to keep in mind when you build on top of the array:
- It contains dismissed and confirmed matches too, so it doubles as the review history.
- It leaves out matches whose other payable is
canceled. A copy that was already cancelled no longer needs a warning. Tesouro still records these matches: to find payables that match a cancelled payable, such as an invoice resent after fraud, use theduplicate_matched_payable_statusfilter. Theirduplicatesarray stays empty for that match. - Suspected matches come first, then confirmed, then dismissed. Within each group the newest match comes first. So if the first entry is
suspected, the payable needs attention. - It returns at most 100 entries. When a payable has more, the dismissed and confirmed history is cut first, never the suspected matches.
- A match also disappears if the other payable is deleted.
- To tell whether a match is resolved, check
statusorresolved_at. Don’t rely onresolved_by_user_id, which can benullfor a resolved match.
GET /payables/{payable_id} with the matched_payable_id.
Review a duplicate
A user resolves a match in one of two ways. Both decisions are final: once a match is dismissed or confirmed, it can’t be changed.Keep both payables
If the two payables are different invoices that happen to share a number, dismiss the match:payable_id can be either payable of the pair, and duplicate_id is the id from its duplicates array. The response is the updated match:
Cancel the duplicate
If the payable really is a duplicate, cancel it first and then confirm the match. These are two separate requests, because confirming a match does not change the status of any payable.- Cancel the duplicate payable with
POST /payables/{payable_id}/cancel. - Confirm the match:
"status": "confirmed".
If the cancellation succeeds but the confirmation fails, the payable stays cancelled and the match stays suspected. You can retry the confirmation at any time.
Once the copy is cancelled, the match no longer appears in the duplicates array of the original payable, because its other payable is now canceled.
A match can be resolved only once. Dismissing or confirming a match that is already resolved
returns
409 Conflict. After a successful request, disable both actions in your UI.Find payables with duplicates
GET /payables accepts filters that select payables by their duplicate matches. These filters choose which payables are returned. They don’t change the duplicates array of the returned payables.
Every filter asks whether at least one matching entry exists. A payable with one dismissed match and one suspected match is returned by
duplicate_match_status__not_in=dismissed, because the suspected match qualifies.
When you combine several of these filters, all conditions must hold for the same match. A payable is not returned just because one of its matches meets one condition and another match meets the other.
The duplicate_match_status filters follow the same rule as the duplicates array and skip cancelled payables:
- Matches whose other payable is
canceleddon’t count, unless you also pass aduplicate_matched_payable_statusfilter, which then decides on its own. - Payables that are themselves
canceledare not returned.
You don’t need these filters to show a warning next to each row of a payables list. Every row already carries its own
duplicates array, so you can check it directly on the page you have.
Permissions
To view and resolve duplicate matches, organization users need these permissions:
The matches come with the payable, so anyone who can view a payable also sees its
duplicates array.