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

# Integrations and syncs

> Connect an accounting system, test it, choose what it reads, and import invoices with a sync.

A **provider** is an accounting or ERP system Watchdog can connect to. The catalog is the same for
every organization. An **integration** is your organization's connection to one provider; you can
have several per provider. A **sync** is one import run of an integration.

The usual path is:

1. [Find the provider](#1-find-the-provider) and the credentials it needs.
2. [Create the integration](#2-create-the-integration).
3. [Test the saved connection](#3-test-the-saved-connection).
4. Optionally, [choose what it reads](#4-choose-what-it-reads).
5. Optionally, [estimate a sync](#5-estimate-a-sync).
6. [Start a sync](#6-start-a-sync) and [follow its progress](#7-follow-progress).

The examples use a client generated from the OpenAPI document; each function is named after its
`operationId`, so any generated client reads the same.

## 1. Find the provider

`GET /v1/integration-providers` lists every provider with what you need to connect it:

* `connection.method` is `credentials` when you send field values yourself, or
  `provider_authorization` when the customer signs in at the provider instead of step 2. See
  [connect at the provider](#connect-at-the-provider).
* `connection.fields` describes each value to collect: `type` is the JSON value to send (`string`,
  `integer` or `string_array`), `choices` lists the only accepted values when there is a fixed list,
  and `required` says it must be set. A `secret` field is write-only. `label` is English; show your
  own copy and logos by provider `key`.
* `capabilities` says which `scope` fields the provider honours, and whether it supports sync
  estimates and realtime updates.

A field with `options_source: "accounts"` is chosen from the provider's own accounts: send the other
fields to `POST /v1/integrations/accounts` and pick from the returned `code`s (`suggested` marks the
likely ones).

## 2. Create the integration

`POST /v1/integrations` (Admin) saves the connection as `not_tested`. Creating never contacts the
provider.

```ts theme={null}
const { data: integration, response } = await createIntegration({
  client,
  body: { provider: 'tripletex', name: 'Tripletex AS', credentials: { employee_token: token } },
});
let etag = response.headers.get('ETag') ?? undefined;
```

To check credentials before anything is saved, `POST /v1/integrations/test` with the same
`provider` and `credentials` runs the same test as step 3. It is optional.

## 3. Test the saved connection

`POST /v1/integrations/{id}/test` with no body signs in, lists invoices within the scope and opens
one attachment, then saves the resulting `status`. A passing test makes the integration `syncable`.
A connection that does not work is still a `200`, with `result: "failed"`; `steps` names the step
and category that failed.

```ts theme={null}
const { data: test } = await testIntegration({ client, path: { id: integration.id } });
if (test.result === 'failed') showFailure(test.steps);
```

## 4. Choose what it reads

The integration's `scope` is what every sync reads, including the nightly one when `auto_sync` is
on. Set it with `PATCH /v1/integrations/{id}` (Admin). To choose from what the provider has,
`GET /v1/integrations/{id}/suppliers` and `…/companies` list them with `search` and paging. They are
served from the last list read from the provider: start one with `POST …/suppliers/refresh` (or
`…/companies/refresh`), then poll the list until `refresh_status` is no longer `running`.

A supplier `include` list reads only the listed suppliers. `include_unidentified` also reads
invoices whose supplier the provider does not identify, and is on unless you set it to `false`.

### Change settings and credentials

`PATCH` takes any of `name`, `enabled`, `auto_sync`, `scope` and `credentials`. `credentials` is a
merge patch: fields you leave out keep their saved value and `null` clears an optional one. Saved
secrets are never returned; `credentials.secrets_set` lists the ones that have a value. New
credentials reset `status` to `not_tested`, so test again.

Send the `ETag` you last read as `If-Match`. If someone else changed the integration since, the
update is refused with `412` and nothing is saved: read it again and reapply your change.

```ts theme={null}
const { data, error, response } = await updateIntegration({
  client,
  path: { id: integration.id },
  headers: { 'if-match': etag },
  body: { credentials: { employee_token: newToken } },
  throwOnError: false,
});
if (data) {
  etag = response?.headers.get('ETag') ?? undefined;
} else if (error?.error.code === 'precondition_failed') {
  // Changed by someone else: read the current version, then decide whether to apply the edit again.
  const current = await getIntegration({ client, path: { id: integration.id } });
  etag = current.response.headers.get('ETag') ?? undefined;
} else if (error?.error.code === 'sync_in_progress' && error.error.workflow_run_id) {
  // Scope and credentials wait for the running sync: cancel it, or retry when it has finished.
  await cancelWorkflowRun({ client, path: { id: error.error.workflow_run_id } });
}
```

To try an edit before saving it, send it as the body of `POST /v1/integrations/{id}/test` (Admin):
it is tested over the saved values and nothing is saved.

## 5. Estimate a sync

Where `capabilities.estimate` is true, `POST /v1/integrations/{id}/sync-estimates` counts the
invoices a sync with the same body would import, without importing anything. Poll it until `status`
is no longer `running`. `lower_bound` is true when counting stopped at the time limit, and
`configuration_revision_matches` turns false when the scope or credentials changed after it
started.

One estimate runs per integration: while one is running, asking again is
`409 estimate_in_progress`. `GET …/sync-estimates?limit=1` returns it; wait for it, or cancel it
with `DELETE /v1/integrations/{id}/sync-estimates/{estimate_id}` and ask again. Starting a sync
cancels a running estimate once the sync is accepted; a rejected sync leaves it running.

## 6. Start a sync

`POST /v1/integrations/{id}/syncs` reads a period and imports its invoices. `date_from` and
`date_through` are both inclusive; leave them out to read the last month up to today. The response
is `202` with the sync.

```ts theme={null}
const { data: sync } = await startIntegrationSync({
  client,
  path: { id: integration.id },
  body: { date_from: '2026-09-01', date_through: '2026-09-30' },
});
```

Leave `scope` out to read the saved scope. A `scope` in the body replaces the saved one for this run
only, as a whole: a dimension you leave out reads everything, rather than keeping its saved value.
A run scope has no `include_unidentified` or `module_company_codes`; its supplier `include` list
reads only the listed suppliers.

Without Admin, a sync or estimate must read a supplier `include` list: send one as `scope`, or leave
`scope` out when the saved scope is one. Otherwise it is `403`.

One sync runs per integration at a time. While it runs, starting another sync, changing `scope` or
credentials, and deleting the integration return `409 sync_in_progress` with the run in
`workflow_run_id`. Cancel that run, or wait, and try again. Renaming and enabling are not affected.

## 7. Follow progress

The sync `id` is also a workflow run id. Follow progress with `GET /v1/workflow-runs/{id}`, read
imported and failed invoices from its items, and stop it with `POST /v1/workflow-runs/{id}/cancel`.
See [workflow runs](/api-preview/workflow-runs).

`GET /v1/integrations/{id}/syncs` lists every sync, including the nightly ones. `coverage` is
`partial` when a sync ended without reading everything it was asked to. When `retryable` is true,
`POST …/syncs/{sync_id}/retry` completes it: with no body it continues where discovery stopped and
retries the failed invoices that can succeed on a second attempt; with `item_ids` it retries exactly
those invoices. The same run becomes active again.

## Connect at the provider

A `provider_authorization` provider (Fortnox, e-conomic) replaces step 2: the customer signs in at
the provider, so this needs their browser and a signed-in session, not an API key.

1. Generate a random 32-byte hex `browser_nonce` and keep it in session storage.
2. `POST /v1/integrations/authorizations` with the `provider`, the `browser_nonce`, and either a
   `name` (plus the provider's `connection.fields`) to create an integration or an `integration_id`
   to reconnect one. Send the browser to the returned `url`.
3. The provider sends the browser back to your app with `integration_state` and
   `integration_code` in the URL fragment, or `integration_error` if the customer cancelled.
4. `POST /v1/integrations/authorizations/complete` with `state`, `code` and the same
   `browser_nonce`. The integration is saved as `not_tested`; continue with step 3.

An attempt expires after ten minutes and provider codes are single-use, so a failed or repeated
completion needs a new attempt. Reconnecting must sign in to the same company.

## Realtime updates

For a provider with `capabilities.realtime`, turning on `auto_sync` also subscribes to the
provider's webhooks, so invoice changes arrive without waiting for the nightly sync. Turning it on
needs an enabled integration that passed its test. `realtime.status` on the integration is
`active`, `degraded` or `inactive`; if the provider refused the subscription change, it shows
`degraded` or `inactive` until a scheduled check repairs it within a few hours.
