# Create match

Create a manual match between a transaction and an invoice.

This is the call that records a settlement. It allocates matchedAmount from a bank transaction onto an invoice. invoiceId and matchedAmount are required, and either transactionId or pspEventId. Identify the invoice and transaction with their codes. Invoice and transaction amounts are updated by database triggers. Incompatible direction, currency, or capacity returns 400.

To deny a suggestion, POST the same codes with status rejected. matchedAmount is then omitted. To point an existing payment at another invoice, unlink first with Reconcile transaction unlink_all, then create the new allocation here. Do not use Update match to change the invoice.

## See also

- [Matches](https://www.invunion.com/knowledge-base/api/matches/). Which call to use for each change.
- [Reconcile transaction](https://www.invunion.com/knowledge-base/api/reconcile-transaction/) (`POST /api/v1/transactions/:code/reconcile`). Ignore a bank line, bring it back into matching, or unlink its matches.
- [List matches](https://www.invunion.com/knowledge-base/api/list-matches/) (`GET /api/v1/matches`). Find an existing allocation by invoice number and transaction code.

- HTTP method: `POST`
- Path: `/api/v1/matches`
- URL: `https://api.invunion.com/api/v1/matches`
- Required scope: `matches:write`
- HTML docs: https://www.invunion.com/knowledge-base/api/create-match/
- Markdown docs: https://www.invunion.com/knowledge-base/api/create-match.md



## Body parameters

| Name | Type | Required | Description | Allowed values | Example |
| --- | --- | --- | --- | --- | --- |
| `invoiceId` | string, no maximum | required | Invoice number. |  | `INV-2026-0042` |
| `transactionId` | string, no maximum | required | Bank transaction code (`transaction_code`). Required unless pspEventId is sent. |  | `TX-8891` |
| `pspEventId` | string, no maximum | optional | PSP event UUID, used instead of a bank transaction. |  | `3fa85f64-5717-4562-b3fc-2c963f66afa6` |
| `matchedAmount` | number | required | Positive amount to allocate. Omit when status is rejected. |  | `1000` |
| `confidenceScore` | number | optional | 0-100. Default 100 for a manual match. |  | `100` |
| `notes` | string, max 500 | optional | Free-text reason, stored as ai_reasoning. |  | `Manual match INV-2026-0042` |
| `status` | string, no maximum | optional | Set to rejected to deny this invoice and transaction pair without allocating. invoiceId and transactionId are required; matchedAmount is not. | `rejected` |  |

## 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` | Wait and retry. The Retry-After header is the number of seconds to wait. |
| `Validation failed` | `400` | The body failed schema validation. details lists field and message. |
| `matchedAmount is required` | `400` | matchedAmount was omitted on a live create. |
| `Either transactionId or pspEventId must be provided` | `400` | Neither source was sent. |
| `Transaction not found` | `400` | transactionId does not exist in this tenant. |
| `An ignored transaction cannot be matched` | `400` | The transaction was ignored. Include it before matching. |
| `Invoice not found` | `400` | invoiceId does not exist in this tenant. |
| `Invoice cannot be matched while its status is paid` | `400` | The invoice is already paid. cancelled invoices have a similar message. |
| `Transaction and invoice currencies must match` | `400` | Currencies differ and no conversion applies. |
| `Invoice is already fully paid` | `400` | open_amount is already zero. |
| `A match already exists between this source and invoice` | `400` | A live match already links this pair. |
| `Internal server error` | `500` | Unexpected server error. The JSON body includes correlationId. Retry with backoff. |

## Request (curl)

```bash
curl --request POST \
  --url https://api.invunion.com/api/v1/matches \
  --header 'accept: application/json' \
  --header 'authorization: Bearer uk_live_YOUR_API_KEY' \
  --header 'content-type: application/json' \
  --data '{
  "invoiceId": "INV-2026-0042",
  "transactionId": "TX-8891",
  "matchedAmount": 1000,
  "notes": "Manual match INV-2026-0042"
}'
```

## Request (Python)

```python
import requests

url = "https://api.invunion.com/api/v1/matches"
headers = {
    "Accept": "application/json",
    "Authorization": "Bearer uk_live_YOUR_API_KEY",
    "Content-Type": "application/json",
}
payload = {
    "invoiceId": "INV-2026-0042",
    "transactionId": "TX-8891",
    "matchedAmount": 1000,
    "notes": "Manual match INV-2026-0042"
}
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")
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  \"invoiceId\": \"INV-2026-0042\",\n  \"transactionId\": \"TX-8891\",\n  \"matchedAmount\": 1000,\n  \"notes\": \"Manual match INV-2026-0042\"\n}"
response = http.request(request)
puts response.body
```

## Request (JavaScript)

```javascript
const response = await fetch("https://api.invunion.com/api/v1/matches", {
  method: "POST",
  headers: {
    "Accept": "application/json",
    "Authorization": "Bearer uk_live_YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "invoiceId": "INV-2026-0042",
  "transactionId": "TX-8891",
  "matchedAmount": 1000,
  "notes": "Manual match INV-2026-0042"
}),
});
const data = await response.json();
```

## Request (Go)

```go
package main

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

func main() {
	payload := []byte(`{
  "invoiceId": "INV-2026-0042",
  "transactionId": "TX-8891",
  "matchedAmount": 1000,
  "notes": "Manual match INV-2026-0042"
}`)
	req, err := http.NewRequest("POST", "https://api.invunion.com/api/v1/matches", 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", {
  method: "POST",
  headers: {
    "Accept": "application/json",
    "Authorization": "Bearer uk_live_YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  "invoiceId": "INV-2026-0042",
  "transactionId": "TX-8891",
  "matchedAmount": 1000,
  "notes": "Manual match INV-2026-0042"
}),
});
console.log(await response.json());
```

## Success (201)

```json
{
  "success": true,
  "data": {
    "match": {
      "id": "1c9e6679-7425-40de-944b-e07fc1f90ae7",
      "transaction_id": "2d0e6679-7425-40de-944b-e07fc1f90ae7",
      "invoice_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "matched_amount": 1000,
      "match_type": "manual",
      "confidence_score": 100,
      "status": "active",
      "created_at": "2026-09-12T09:00: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"
    }
  }
}
```
