> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tesouro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Duplicate payables

> Learn how Tesouro flags possible duplicate payables and how your users review them.

## Overview

Tesouro flags payables that look like duplicates of each other. It never cancels a payable on its own: an [organization user](/finops/support/glossary#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](/finops/guides/common/counterparts/index), 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.

A payable without an invoice number never matches anything, and an empty invoice number is treated the same as a missing one.

A duplicate match always links exactly two payables and is visible from both of them. Either payable can be used to review it, and the result is the same.

## 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](/finops/guides/accounts-payable/payables/collect#create-a-payable-from-data);
* [OCR](/finops/guides/accounts-payable/payables/collect#about-tesouro-ocr) finishes processing an uploaded or emailed invoice;
* a payable is [updated](/finops/guides/accounts-payable/payables/manage#update-a-payable) and its `counterpart_id` or `document_id` changes;
* a counterpart is [linked automatically](/finops/guides/common/counterparts/index#auto-linking) while the payable moves through its lifecycle, for example on submission for approval.

An update can also remove a warning. If a user corrects the invoice number of a payable and it no longer collides with anything, the pending match disappears.

## 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:

```json lines theme={null}
{
  "id": "aa314fdd-a763-4920-a8c8-6285fc1745c0",
  "status": "new",
  "document_id": "INV-2031",
  "counterpart_id": "5940eb35-d9f1-4d4d-a2b4-5d8a8c1eb2a2",
  ...,
  "duplicates": [
    {
      "id": "e3c0f2b9-2f5a-4c7b-9b1e-6f2d9a7c1d44",
      "status": "suspected",
      "matched_payable_id": "0b1f6c1e-3d2a-4e8f-9c55-8a7e2b4d9f10",
      "matched_payable_status": "paid",
      "resolved_by_user_id": null,
      "resolved_at": null,
      "created_at": "2026-09-01T10:12:44.018Z",
      "updated_at": "2026-09-01T10:12:44.018Z"
    }
  ]
}
```

| Field | Description |
| - | - |
| `id` | ID of the duplicate match. Use it to dismiss or confirm the match. Both payables of the pair show the same `id`. |
| `status` | `suspected` means the match is waiting for review, `dismissed` means a user marked it as not a duplicate, and `confirmed` means a user confirmed it as a duplicate. |
| `matched_payable_id` | ID of the other payable of the pair. On each side of the pair this field points at the opposite payable. |
| `matched_payable_status` | Current status of the other payable. It always reflects the latest state, so you can show whether the original was already paid without fetching it. |
| `resolved_by_user_id` | The organization user who dismissed or confirmed the match. It is `null` while the match is suspected, and also for decisions made with a partner access token. |
| `resolved_at` | When the match was dismissed or confirmed. |

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 the [`duplicate_matched_payable_status` filter](#find-payables-with-duplicates). Their `duplicates` array 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 `status` or `resolved_at`. Don't rely on `resolved_by_user_id`, which can be `null` for a resolved match.

To show details of the other payable, such as its invoice number or amount, call `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:

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/finops/v1/payables/{payable_id}/duplicates/{duplicate_id}/dismiss' \
     -H 'X-Finops-Version: 2025-06-23' \
     -H 'X-Organization-Id: ORGANIZATION_ID' \
     -H 'Authorization: Bearer ACCESS_TOKEN'
```

`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:

```json lines theme={null}
{
  "id": "e3c0f2b9-2f5a-4c7b-9b1e-6f2d9a7c1d44",
  "status": "dismissed",
  "payable_id": "aa314fdd-a763-4920-a8c8-6285fc1745c0",
  "matched_payable_id": "0b1f6c1e-3d2a-4e8f-9c55-8a7e2b4d9f10",
  "resolved_by_user_id": "91b2a4c7-6e0f-4a3d-8f21-3c5d7e9b0a16",
  "resolved_at": "2026-09-01T11:03:27.442Z",
  "created_at": "2026-09-01T10:12:44.018Z",
  "updated_at": "2026-09-01T11:03:27.442Z"
}
```

A dismissed pair is never flagged again, even if one of the payables is edited later and its invoice number collides again.

### 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.

1. Cancel the duplicate payable with [`POST /payables/{payable_id}/cancel`](/finops/guides/accounts-payable/approvals/manual-transition#cancel-a-payable).
2. Confirm the match:

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/finops/v1/payables/{payable_id}/duplicates/{duplicate_id}/confirm' \
     -H 'X-Finops-Version: 2025-06-23' \
     -H 'X-Organization-Id: ORGANIZATION_ID' \
     -H 'Authorization: Bearer ACCESS_TOKEN'
```

The response has the same shape as for a dismissal, with `"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`.

<Info>
  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.
</Info>

## Find payables with duplicates

[`GET /payables`](/finops/reference/openapi/payables/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.

| Parameter | Returns payables that have at least one match where |
| - | - |
| `duplicate_match_status`, `duplicate_match_status__in`, `duplicate_match_status__not_in` | the match status is (or is not) one of the given values |
| `duplicate_matched_payable_status`, `duplicate_matched_payable_status__in`, `duplicate_matched_payable_status__not_in` | the other payable of the pair is (or is not) in one of the given statuses |

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 `canceled` don't count, unless you also pass a `duplicate_matched_payable_status` filter, which then decides on its own.
* Payables that are themselves `canceled` are not returned.

Some examples:

| Endpoint | Description |
| - | - |
| `GET /payables?duplicate_match_status=suspected` | Get active payables with a duplicate waiting for review. |
| `GET /payables?duplicate_match_status=confirmed` | Get active payables that are part of a confirmed duplicate pair. |
| `GET /payables?duplicate_match_status=suspected&duplicate_matched_payable_status=canceled` | Get payables that match a cancelled payable, for example an invoice resent after fraud. |

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](/finops/guides/roles/permissions#payables-ap):

| Action | Permission key |
| - | - |
| View matches of a payable | `payable:read:org` |
| Dismiss or confirm a match | `payable:write:org` |
| Cancel the duplicate payable | `payable:write:org` |

The matches come with the payable, so anyone who can view a payable also sees its `duplicates` array.
