# Payment reconciliation

[`GET /api/v1/payments`](api-reference.md#listPayments) returns the payments of your
project, newest first, in pages. Every item is the same object as the
`GET /api/v1/payments/{id}` response: status, amounts, requisites and the
`merchant_information` settlement. Payments of other projects are never listed.

!!! note "Test payments in the list"
    Test payments (sandbox, project test mode) are listed together with live ones and are
    marked with `is_test: true`; live payments have `is_test: false`. Every item has the
    field. To reconcile money, use `is_test=false` — live payments only.

What it is for:

- **Reconciling a period.** Once a day (or more often) walk all payments of the past day
  and compare statuses and amounts with your orders.
- **Recovering after a failure.** Your server lost a create response or missed callbacks —
  find the payment by your order id or walk the list for the time of the outage.
- **Lookup by order id.** `merchant_payment_id` is unique within the project: filtering by
  it returns one payment, no `id` needed.

The token needs the `payments:read` scope. Signing is the same as for other reads: only
when the project policy requires it.

## Request { #request }

```http
GET /api/v1/payments?status=completed,canceled&is_test=false&created_from=2026-10-01T00:00:00%2B03:00&created_to=2026-10-02T00:00:00%2B03:00&limit=100
Authorization: Bearer nl_test_...
```

| Parameter | Meaning |
| --- | --- |
| `status` | Only these statuses, comma-separated: `completed,canceled`. The parameter may also be repeated. Omitted — any status |
| `merchant_payment_id` | Only the payment with this order id, exact match |
| `created_from` | Created at or after this moment. RFC 3339 with a time zone |
| `created_to` | Created before this moment (not including it). RFC 3339 with a time zone |
| `cursor` | `next_cursor` of the previous page. Omitted — the first page |
| `limit` | Payments per page, 1 to 100. 50 by default |
| `is_test` | `true` — only test payments, `false` — only live ones. Omitted — both |

All parameters are optional and combine with AND. Encode the `+` of a time zone in the URL
as `%2B`, otherwise it reads as a space — or write the time in UTC:
`2026-09-30T21:00:00Z`.

## Response { #response }

```json
{
  "items": [
    {
      "id": "3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42",
      "merchant_payment_id": "order-42",
      "status": "completed",
      "amount": 3400,
      "currency": "RUB",
      "is_test": false,
      "...": "other fields as in GET /api/v1/payments/{id}"
    }
  ],
  "next_cursor": "MTc5MDY3NDIwMDAwMDAwMDAwMHwzZjFjMmI5ZS02YThkLTRjNGItOWE3MS0yYzdmNWQxZTBhNDI"
}
```

- `items` — the payments of the page, newest first; an empty array when there are none.
- `next_cursor` — a string when there is a next page, `null` on the last one. The cursor
  is opaque: do not parse it or build it yourself.

## Walking all pages { #pagination }

Request the first page, then repeat the same request with `cursor` set to `next_cursor`
until `next_cursor` is `null`. Pass **the same** filters with every page.

```python
params = {"created_from": "2026-10-01T00:00:00Z", "created_to": "2026-10-02T00:00:00Z", "is_test": "false", "limit": 100}
while True:
    page = get("/api/v1/payments", params)  # sign it if the project policy requires it
    for payment in page["items"]:
        reconcile(payment)  # compare status and amount with your order
    if page["next_cursor"] is None:
        break
    params["cursor"] = page["next_cursor"]
```

- Payments are ordered by creation time, then by `id`. A walk neither repeats nor skips
  payments, even ones created at the same moment.
- Payments created after the walk started are not in it. To see them, start a new walk —
  for example with `created_from` set to the start of the previous one.
- A status may change during the walk: an item shows the status at the moment its page was
  read. To reconcile a closed period, take payments older than the project payment
  lifetime — their status is already final (except for a payment re-check, see
  [Status changes after closing](callbacks.md#status-changes)).
- The project shares one [request limit](idempotency.md#rate-limits): for long periods use
  `limit=100` and do not run several walks in parallel.

## Lookup by order id { #by-order }

```http
GET /api/v1/payments?merchant_payment_id=order-42
Authorization: Bearer nl_test_...
```

The response has one payment, or an empty `items` when the project has no payment with
this order id. The id has the same format as on create: 1–64 characters
`A-Z a-z 0-9 . _ : -`.

## Errors { #errors }

A wrong parameter returns `400`, nothing is read:

| `error` | When |
| --- | --- |
| `invalid_status` | A status is not one of the [payment statuses](statuses.md#statuses) |
| `invalid_merchant_payment_id` | `merchant_payment_id` does not match the order id format |
| `invalid_date_range` | `created_from` or `created_to` is not RFC 3339 with a time zone, or `created_from` is later than `created_to` |
| `invalid_cursor` | `cursor` is not a `next_cursor` of this API |
| `invalid_limit` | `limit` is not a whole number from 1 to 100 |
| `invalid_is_test` | `is_test` is neither `true` nor `false` |

Without the `payments:read` scope — `403 insufficient_scope`. Other errors are as for any
request, see [Statuses and errors](statuses.md#http-errors).