# Accounting connectors

> QuickBooks Online, Xero and Odoo: OAuth or credentials, pull contacts and invoices, push issued invoices, and the link table that keeps both sides in step.

Source: /docs/guides/connectors

Alya Books syncs with QuickBooks Online, Xero and Odoo (plus the older Zoho Books and Tally connectors). A connector is set up in the app under **Connectors**; the API can list connectors, run syncs and inspect the record links. Credentials never leave the database: `GET /connectors` strips `config.credentials`, and in the app only the server reads them (members see the settings without tokens or keys).

## Connection models

| Provider | Auth | Where it is set up |
| --- | --- | --- |
| `quickbooks` | OAuth 2.0 (Intuit) | App → Connectors → Connect. The `oauth-initiate` function builds the authorize URL, `oauth-callback` exchanges the code and stores tokens; needs `QUICKBOOKS_CLIENT_ID` / `QUICKBOOKS_CLIENT_SECRET` and `APP_URL` on the server. |
| `xero` | OAuth 2.0 | Same flow with `XERO_CLIENT_ID` / `XERO_CLIENT_SECRET`. |
| `odoo` | URL, database, login, API key (JSON-RPC) | App → Connectors → Connect Odoo (form). **Test** signs in and stores the user id. |

Every provider also has a **sandbox** mode (`config.mode = "sandbox"`) that serves sample data and accepts pushes without contacting anyone. Use it to exercise sync and push in CI or on a demo organization.

## Run a sync

```bash
curl -X POST "$BASE/connectors/$CONNECTOR_ID/sync" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{ "mode": "incremental", "pushInvoices": true }'
```

`mode` is `full`, `incremental` (changes since `lastSyncAt`) or `date_range` (`dateFrom`, `dateTo`). `pullContacts`, `pullInvoices` and `pushInvoices` override the connector's configuration for this run; `pushSince` (YYYY-MM-DD) limits which issued invoices are pushed (default: the connector's `push_since`, else the last 30 days). Only keys with `settings:write` may sync; Zoho Books and Tally keep their own functions and return `409`.

The response is a summary:

```json
{
  "contacts": { "imported": 12, "updated": 3, "skipped": 1, "errors": 0 },
  "invoices": { "imported": 8, "skipped": 2, "errors": 0 },
  "pushed": { "created": 5, "skipped": 0, "errors": 1, "details": [{ "invoiceId": "…", "number": "INV-2027-00012", "error": "Unknown tax code" }] },
  "companyName": "Acme Trading LLC",
  "messages": []
}
```

The sync is recorded in the connector's history and emits `connector.synced`.

## What moves

- **Contacts** are matched by TRN, then case-insensitively by name; a match links the existing contact instead of creating one. Later runs update linked contacts.
- **Invoices pulled** become drafts with `sourceSystem` = provider and `sourceId` = the remote id. Lines keep quantity, unit price, VAT category (mapped from the remote tax) and the remote number. A remote invoice whose number already exists in Alya Books is skipped.
- **Invoices pushed** are issued invoices (`submitted`, `delivered`, `accepted`; not test, not archived, not themselves imported from that provider) issued on or after `pushSince`. Each is created once; the link table prevents a second push. Tax codes and income account come from the connector config (`tax_code_standard`, `tax_code_zero_rated`, `tax_code_exempt`, `income_account`).

## Record links

`GET /connectors/{id}/links?entityType=invoice` pages the `Alya Books id ↔ remote id` pairs with `direction` (`pull` or `push`), `remoteUpdatedAt` and `lastSyncedAt`. Use it to reconcile or to find the QuickBooks/Xero/Odoo id of a Alya Books invoice.

## Scheduling

Connectors with `sync_frequency` = `daily` are synced by the daily jobs (`daily-jobs`, at 02:00 and 18:45 UAE time). Manual connectors sync only on request. Tokens are refreshed automatically before each run; a refresh failure sets the connector to `error` with the message in `lastSyncMessage`, and **Test** in the app (or a new OAuth round trip) repairs it.

## MCP and CLI

MCP tools `list_connectors` and `sync_connector`. CLI: `alyabooks connector list`, `alyabooks connector sync <id> --mode incremental --push`.
