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

# Incremental Sync

> Keep a local copy of transactions current without refetching everything

[`GET /transactions`](/docs/api-reference/transactions/list-transactions) has an incremental sync mode for consumers that keep their own copy of a company's transactions. Instead of paging through the entire list on every run and diffing it yourself, you ask "what changed since I last looked?" and receive only the transactions modified since then, including the ones that were deleted.

Supplying `updated_after` (a Unix timestamp in integer seconds, not milliseconds) switches the endpoint into incremental mode. A request without it behaves exactly as it always has, so existing consumers see no difference.

## What changes in incremental mode

| | Classic listing (no `updated_after`) | Incremental sync (`updated_after` supplied) |
| - | - | - |
| Ordering | `date` descending, newest transaction date first | `updated` ascending, oldest **modification** first |
| `next_cursor` | A transaction id (e.g. `txn_a1b2c3d4e5f6g7`) | An opaque token. Do not parse it |
| Deleted transactions | Omitted entirely | Included, carrying `deleted: true` |
| `watermark` in the envelope | Absent | Present |

Two fields on the transaction object support this mode, and they appear on **every** endpoint that returns a transaction (list, retrieve, and update):

| Field | Type | Meaning |
| - | - | - |
| `updated` | integer | Unix timestamp of when the transaction was last modified. This is the value `updated_after` compares against. |
| `deleted` | boolean | Whether the transaction has been deleted. Always `false` outside incremental sync, because that is the only mode that returns deleted transactions at all. |

Note that `updated_after` filters on when a transaction was last **modified**, which is unrelated to `start_date` and `end_date`, which filter on the transaction's own **date**. A transaction dated last January can have been modified this morning.

## The watermark: use the server's clock, never your own

Every incremental response carries a top-level `watermark` field: a server-generated Unix timestamp that you pass back as `updated_after` on your next sync.

**Always pass back the watermark. Never compute the next `updated_after` from your own clock.** Consumer clocks drift relative to the server's, and drift in the fast direction silently loses records: you ask for changes since a moment that had not happened yet on the server, and anything written in the gap is skipped with no error and no visible symptom.

The watermark is pinned when a sync starts and is **identical on every page of that sync**. That is deliberate: it lets you drain all pages of a run and commit a single value at the end.

Treat the watermark as a value to round-trip, not a value to do arithmetic on, even though it is a timestamp. Do not add a safety margin, subtract a few seconds, or compare it against your local clock.

## The sync loop

1. Call `GET /transactions?updated_after=<your stored watermark>`.
2. Process `data`. Apply updates for rows with `deleted: false`, remove rows with `deleted: true`.
3. If `has_more` is true, call again with `starting_after=<next_cursor>` **and the same `updated_after`**. Repeat.
4. When `has_more` is false, store `watermark` as your new checkpoint.

Do not store the watermark until the last page arrives. If a sync is interrupted halfway, discard the partial run and restart from your previously stored watermark. The overlap is harmless, because applying the same change twice leaves your copy where it was, whereas committing a watermark early skips everything you had not yet paged through.

First page:

```bash theme={null}
curl "https://app.finta.com/api/v1/transactions?updated_after=1771097200&limit=2" \
  -H "Authorization: Bearer finta_your_key_here"
```

```json theme={null}
{
  "object": "list",
  "url": "/api/v1/transactions",
  "has_more": true,
  "next_cursor": "WzEsMTc3MTE0MDQwMDAwMDAwMCwyODQxMiwxNzcxMTgzNjAwMDAwMDAwXQ",
  "watermark": 1771183600,
  "data": [
    {
      "id": "txn_a1b2c3d4e5f6g7",
      "object": "transaction",
      "created": 1771097300,
      "updated": 1771140400,
      "deleted": false
    },
    {
      "id": "txn_h8i9j0k1l2m3n4",
      "object": "transaction",
      "created": 1770991000,
      "updated": 1771150900,
      "deleted": true
    }
  ]
}
```

Next page, with the same `updated_after` plus the cursor:

```bash theme={null}
curl "https://app.finta.com/api/v1/transactions?updated_after=1771097200&limit=2&starting_after=WzEsMTc3MTE0MDQwMDAwMDAwMCwyODQxMiwxNzcxMTgzNjAwMDAwMDAwXQ" \
  -H "Authorization: Bearer finta_your_key_here"
```

```json theme={null}
{
  "object": "list",
  "url": "/api/v1/transactions",
  "has_more": false,
  "next_cursor": null,
  "watermark": 1771183600,
  "data": [
    {
      "id": "txn_o5p6q7r8s9t0u1",
      "object": "transaction",
      "created": 1771160000,
      "updated": 1771160000,
      "deleted": false
    }
  ]
}
```

`has_more` is false, so `1771183600` becomes the stored checkpoint for the next run. Note the watermark is the same `1771183600` on both pages. The transaction objects above are abbreviated; real responses carry the full [`Transaction`](/docs/api-reference/transactions/list-transactions) object on every row.

<Note>
  **Changes made while a sync runs are not lost.** A run is bounded at the instant its watermark records, so a transaction modified mid-run simply arrives on your next sync. In rare cases, a write that was still committing while your run read past it can land just behind the watermark. If your use case cannot tolerate even that window, occasionally restart from a checkpoint a few seconds older than the one you stored and let the harmless overlap re-apply. That is the one sanctioned exception to the no-arithmetic rule above; never adjust the watermark in your normal loop.
</Note>

## Bootstrapping a mirror: start at `updated_after=0`

**To seed a new mirror, call `GET /transactions?updated_after=0`, not the plain list.** Zero matches every transaction, so the initial load and every later sync share one mode, one ordering, and one cursor format, and the watermark the initial load returns is exactly what the next run passes back, with no gap between them.

Seeding from the plain list is the trap this design exists to prevent: the plain list returns no watermark, so you would have to invent a starting point from your own clock, reintroducing exactly the drift the watermark removes, and losing anything written between the seed call and your first incremental call.

One cost of starting at zero: the sync also returns tombstones for transactions that were deleted before your mirror existed. You have no copy of them to remove, so skip any `deleted: true` row you do not already hold. This is harmless.

## Deletions

The classic listing has always hidden deleted transactions, and the retrieve and update endpoints do not return them either. A deleted row simply stopped appearing, and there was no way to tell "deleted" apart from "fell outside my page window" without a full re-read.

Incremental mode is the exception: a row with `deleted: true` is a tombstone. The transaction was genuinely deleted, and you should remove it from your copy. `deleted: true` is never used to signal any other kind of disappearance; see the filter caveat below for the one situation where a row vanishes without a tombstone.

## Combining filters with `updated_after`

The list's filters may be combined with `updated_after` and are applied normally. **Know what you are getting, because there is a real hazard here.**

Filters match a transaction's *current* state. A transaction that changes such that it no longer matches your filter simply stops appearing in your sync results, with no `deleted` tombstone, because it was not deleted. Concretely: you sync with `status=approved`, a transaction you already hold moves back to pending, and your next sync just does not mention it. Your copy still says approved, and nothing told you otherwise.

<Warning>
  **If you are maintaining a persistent mirror, a filtered sync means your copy can only grow.** Rows enter it correctly and never leave, and the drift is silent. Sync with `updated_after` alone and apply your filters on your own side.
</Warning>

The combination is safe in two situations:

* **You are asking a question, not maintaining a copy.** A one-shot query that keeps no state cannot drift, because there is nothing persistent to go stale.
* **You are filtering on something that cannot change.** `source_vendor` and `source_type` are fixed at ingestion, and `start_date`/`end_date` filter on the transaction's immutable `date`, so those are safe.

## Errors

**Invalid `updated_after` returns [`400 invalid_request`](/docs/api-reference/errors#invalid_request).** The value must be a non-negative integer no greater than `253402300799` (9999-12-31). The upper bound catches the classic mistake: a JavaScript consumer passing `Date.now()` sends *milliseconds*, roughly 1000x too large. The same `400` is returned for a non-integer value and for a negative value.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "updated_after is invalid.",
    "doc_url": "https://www.finta.com/docs/api-reference/errors#invalid_request",
    "param": "updated_after"
  }
}
```

**An unreadable cursor returns [`404 resource_missing`](/docs/api-reference/errors#resource_missing).** The incremental `next_cursor` is an opaque token. Handing back something the server did not issue (a truncated string, a transaction id, a hand-edited value) is rejected rather than half-trusted:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_missing",
    "message": "The transaction cursor was not found.",
    "doc_url": "https://www.finta.com/docs/api-reference/errors#resource_missing",
    "param": "starting_after"
  }
}
```

Recovery: discard the cursor and restart the sync from your last stored watermark.

The classic listing can return the same `404` for an unknown transaction id passed as `starting_after`, with a generic message and no `param`. Both cases are declared on the list operation.

## Consuming incremental sync safely

* **Treat `next_cursor` as opaque in incremental mode.** It is an encoded internal token. Do not parse it, construct one, or assume it is a transaction id. In the classic listing it remains a transaction id.
* **Handle `deleted` defensively everywhere.** It may be `true` only in incremental mode today, but read it wherever you consume a transaction rather than assuming it is always `false` elsewhere.
* **Carry `updated_after` on every page of a sync**, not just the first. It is part of the query, and the cursor alone does not replace it.
* **Expect rows you have never seen.** Any consumer that starts using `updated_after` begins receiving transaction objects for deleted rows it previously could not see at all.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.