Skip to main content
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 and the credentials it needs.
  2. Create the integration.
  3. Test the saved connection.
  4. Optionally, choose what it reads.
  5. Optionally, estimate a sync.
  6. Start a sync and follow its 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.
  • 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 codes (suggested marks the likely ones).

2. Create the integration

POST /v1/integrations (Admin) saves the connection as not_tested. Creating never contacts the provider.
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.

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.
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.
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. 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.