# Reverse match

Reverse a wrong payment with a reason, and optionally match the right transaction.

Use this when a payment recorded on an invoice was wrong: the invoice was never paid by that transaction, the amount was wrong, or the payment was counted twice. The match must be active or confirmed. The path is the match UUID from List matches, Get invoice, or a match webhook, not from the product.

The match is not deleted. It is kept as cancelled with who reversed it, when, the reason, and the note. Invoice and transaction amounts are recalculated, and the invoice reopens. reason wrong_transaction also records the pair as rejected, so matching does not suggest it again; the response returns that row as rejectedPairId.

Send replacement to match the transaction that really paid the invoice in the same step. replacement.transactionId is a transaction code. matchedAmount defaults to the reversed amount. The replacement is checked like Create match.

One invoice.settlement_changed webhook carries the removal, with its reason, and the replacement. match.cancelled is also emitted, and match.rejected for a wrong_transaction pair.

einvoicing is null unless an e-invoicing payment report exists for this payment. needsCorrection true means the report was already sent and needs a correction.

## See also

- [Matches](https://www.invunion.com/knowledge-base/api/matches/). Which call to use for each change.
- [List matches](https://www.invunion.com/knowledge-base/api/list-matches/) (`GET /api/v1/matches`). Get the UUID by invoice number and transaction code.
- [Create match](https://www.invunion.com/knowledge-base/api/create-match/) (`POST /api/v1/matches`). The checks applied to replacement.
- [Webhooks](https://www.invunion.com/knowledge-base/api/webhooks/). The invoice.settlement_changed payload.

- HTTP method: `POST`
- Path: `/api/v1/matches/:id/reverse`
- URL: `https://api.invunion.com/api/v1/matches/:id/reverse`
- Required scope: `matches:write`
- Idempotency-Key header: accepted. See https://www.invunion.com/knowledge-base/api/idempotency/
- HTML docs: https://www.invunion.com/knowledge-base/api/reverse-match/
- Markdown docs: https://www.invunion.com/knowledge-base/api/reverse-match.md

## Path parameters

| Name | Type | Required | Description | Allowed values | Example |
| --- | --- | --- | --- | --- | --- |
| `id` | string, no maximum | required | Match UUID. |  | `1c9e6679-7425-40de-944b-e07fc1f90ae7` |


## Body parameters

| Name | Type | Required | Description | Allowed values | Example |
| --- | --- | --- | --- | --- | --- |
| `reason` | string, no maximum | required | Why the payment was wrong. wrong_transaction also records the pair as rejected. | `wrong_transaction`, `wrong_amount`, `duplicate`, `other` |  |
| `note` | string, max 500 | optional | Free text kept with the reversal. |  | `Paid by the parent company, not Leones Cars` |
| `replacement` | object | optional | The transaction that really paid the invoice: { transactionId, matchedAmount? }. transactionId is the transaction code. matchedAmount is a positive number and defaults to the reversed amount. |  | `{"transactionId":"TX-8892"}` |

## Errors

| Error | HTTP code | Description |
| --- | --- | --- |
| `Missing Bearer token` | `401` | The Authorization header is missing or is not a Bearer token. |
| `Invalid or revoked API key` | `401` | The API key is unknown, malformed, expired, or has been revoked. |
| `API key is missing scope matches:write` | `403` | The key does not include matches:write. A write scope does not imply the matching read scope. |
| `Too many requests, please try again later` | `429` | More than 100 requests in one minute from this API key or IP address. Wait for the Retry-After header (seconds) and retry. |
| `Validation failed` | `400` | The body failed schema validation, for example an unknown reason. details lists field and message. |
| `Transaction not found` | `400` | replacement.transactionId does not exist in this tenant. The other Create match checks apply to replacement too. |
| `Match not found` | `404` | No match with this UUID exists in the authenticated tenant. |
| `Only a payment that settles an invoice can be reversed.` | `409` | The match is pending_review, rejected, or already cancelled. |
| `Internal server error` | `500` | Unexpected server error. The JSON body includes correlationId. Retry with backoff. |
| `Idempotency-Key must be 8-255 characters: letters, digits, ".", "_", ":" or "-".` | `400` | The Idempotency-Key header is present but malformed. |
| `A request with this Idempotency-Key is still being processed.` | `409` | The first request with this key has not finished. Retry after it completes. code is idempotency_in_progress. |
| `This Idempotency-Key was already used with a different request body.` | `422` | Send a new key for a different request. code is idempotency_key_reuse. |

## Request (curl)

```bash
curl --request POST \
  --url https://api.invunion.com/api/v1/matches/1c9e6679-7425-40de-944b-e07fc1f90ae7/reverse \
  --header 'accept: application/json' \
  --header 'authorization: Bearer uk_live_YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --data '{
  "reason": "wrong_transaction",
  "note": "Paid by the parent company, not Leones Cars",
  "replacement": {
    "transactionId": "TX-8892"
  }
}'
```

## Request (Python)

```python
import requests

url = "https://api.invunion.com/api/v1/matches/1c9e6679-7425-40de-944b-e07fc1f90ae7/reverse"
headers = {
    "Accept": "application/json",
    "Authorization": "Bearer uk_live_YOUR_API_KEY",
    "Content-Type": "application/json",
}
payload = {
    "reason": "wrong_transaction",
    "note": "Paid by the parent company, not Leones Cars",
    "replacement": {
        "transactionId": "TX-8892"
    }
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```

## Request (Ruby)

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI("https://api.invunion.com/api/v1/matches/1c9e6679-7425-40de-944b-e07fc1f90ae7/reverse")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Accept'] = 'application/json'
request['Authorization'] = 'Bearer uk_live_YOUR_API_KEY'
request['Content-Type'] = 'application/json'
request.body = "{\n  \"reason\": \"wrong_transaction\",\n  \"note\": \"Paid by the parent company, not Leones Cars\",\n  \"replacement\": {\n    \"transactionId\": \"TX-8892\"\n  }\n}"
response = http.request(request)
puts response.body
```

## Request (JavaScript)

```javascript
const response = await fetch("https://api.invunion.com/api/v1/matches/1c9e6679-7425-40de-944b-e07fc1f90ae7/reverse", {
  method: "POST",
  headers: {
    "Accept": "application/json",
    "Authorization": "Bearer uk_live_YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "reason": "wrong_transaction",
  "note": "Paid by the parent company, not Leones Cars",
  "replacement": {
    "transactionId": "TX-8892"
  }
}),
});
const data = await response.json();
```

## Request (Go)

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	payload := []byte(`{
  "reason": "wrong_transaction",
  "note": "Paid by the parent company, not Leones Cars",
  "replacement": {
    "transactionId": "TX-8892"
  }
}`)
	req, err := http.NewRequest("POST", "https://api.invunion.com/api/v1/matches/1c9e6679-7425-40de-944b-e07fc1f90ae7/reverse", bytes.NewBuffer(payload))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", "Bearer uk_live_YOUR_API_KEY")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}
```

## Request (Node)

```javascript
const response = await fetch("https://api.invunion.com/api/v1/matches/1c9e6679-7425-40de-944b-e07fc1f90ae7/reverse", {
  method: "POST",
  headers: {
    "Accept": "application/json",
    "Authorization": "Bearer uk_live_YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "reason": "wrong_transaction",
  "note": "Paid by the parent company, not Leones Cars",
  "replacement": {
    "transactionId": "TX-8892"
  }
}),
});
console.log(await response.json());
```

## Success (200)

```json
{
  "success": true,
  "data": {
    "reversed": {
      "id": "1c9e6679-7425-40de-944b-e07fc1f90ae7",
      "invoiceId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "transactionId": "2d0e6679-7425-40de-944b-e07fc1f90ae7",
      "matchedAmount": 1000,
      "rejectedPairId": "4b7a91c2-18e4-4d6f-a8c1-9e2d5f0b73a1",
      "einvoicing": null
    },
    "replacement": {
      "id": "9fbe3506-5c28-41a3-ec25-32619d4fb7e5",
      "transaction_id": "7d9c13e4-3a06-4f81-ca03-104f7b2d95c3",
      "invoice_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "matched_amount": 1000,
      "match_type": "manual",
      "confidence_score": 100,
      "status": "active",
      "created_at": "2026-10-15T10:30:00.000Z"
    },
    "invoice": {
      "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "invoice_number": "INV-2026-0042",
      "amount_incl_vat": 12000,
      "settled_amount": 1000,
      "open_amount": 11000,
      "recovery_percent": 8.33,
      "status": "partial"
    }
  }
}
```
