Taskr provides full bidirectional sync with major accounting platforms,
keeping your financial data consistent across systems.
Supported Platforms
QuickBooks Online
GlobalFull CRUD sync
Real-time webhooks
Multi-entity support
Xero
GlobalFull CRUD sync
Multi-tenant support
Comprehensive API
Entity Mapping
| Taskr Entity | QuickBooks | Xero |
|---|---|---|
| Customer | Customer | Contact (Customer) |
| Vendor | Vendor | Contact (Supplier) |
| Invoice | Invoice | Invoice |
| Payment | Payment | Payment |
| Item/Service | Item | Item |
| Account | Account | Account |
| Tax Rate | TaxCode | TaxRate |
| Expense | Purchase | BankTransaction |
Connection Flow
Sync Patterns
Bidirectional Sync
Sync Direction Rules
| Scenario | Direction | Notes |
|---|---|---|
| New customer in Taskr | Taskr → QBO/Xero | Push to accounting |
| New invoice in Taskr | Taskr → QBO/Xero | Push to accounting |
| Payment in QBO/Xero | QBO/Xero → Taskr | Pull from accounting |
| Item updated in QBO/Xero | QBO/Xero → Taskr | Pull changes |
| Customer edited in both | Last write wins | With conflict detection |
Provider Configuration
- QuickBooks Online
- Xero
OAuth Setup
// apps/workbooks-api/src/rest/auth/quickbooks-oauth.ts
const oauthClient = new OAuthClient({
clientId: env.QBO_CLIENT_ID,
clientSecret: env.QBO_CLIENT_SECRET,
environment: env.QBO_ENV, // sandbox or production
redirectUri: `${env.API_URL}/auth/quickbooks/callback`,
});
Webhook Configuration
// apps/workbooks-api/src/rest/webhooks/register.ts
// QuickBooks sends notifications for:
// - Customer, Vendor, Invoice, Payment, Item changes
// - Account, TaxCode changes
// - Estimate, CreditMemo changes
const webhookEvents = [
"Customer", "Vendor", "Invoice", "Payment",
"Item", "Account", "TaxCode", "Estimate",
];
API Operations
// tocld-core/packages/integrations/src/accounting-providers/src/providers/quickbooks.ts
export class QuickBooksProvider {
async getCustomers(realmId: string): Promise<Customer[]> {
return this.query(`SELECT * FROM Customer WHERE Active = true`);
}
async createInvoice(realmId: string, invoice: InvoiceInput) {
return this.post("/invoice", invoice);
}
async updateCustomer(realmId: string, customer: CustomerUpdate) {
return this.post("/customer", { ...customer, sparse: true });
}
}
Sync Token Handling
QuickBooks uses sync tokens for optimistic locking:// Must include SyncToken for updates
await qbo.updateCustomer({
Id: "123",
SyncToken: "2", // Required - current version
DisplayName: "Updated Name",
});
// If SyncToken is stale, QBO returns conflict error
OAuth Setup
// apps/workbooks-api/src/rest/auth/xero-oauth.ts
const xeroClient = new XeroClient({
clientId: env.XERO_CLIENT_ID,
clientSecret: env.XERO_CLIENT_SECRET,
redirectUris: [`${env.API_URL}/auth/xero/callback`],
scopes: [
"openid", "profile", "email",
"accounting.transactions", "accounting.contacts",
"accounting.settings.read",
],
});
Multi-Tenant Support
Xero supports multiple organizations per connection:// User can select which org to sync
const tenants = await xeroClient.updateTenants();
// Store selected tenant
await db.update(accountingConnections).set({
xeroTenantId: selectedTenant.tenantId,
xeroTenantName: selectedTenant.tenantName,
});
API Operations
// tocld-core/packages/integrations/src/accounting-providers/src/providers/xero.ts
export class XeroProvider {
async getContacts(tenantId: string): Promise<Contact[]> {
return this.api.accountingApi.getContacts(tenantId);
}
async createInvoice(tenantId: string, invoice: Invoice) {
return this.api.accountingApi.createInvoices(tenantId, { invoices: [invoice] });
}
async getInvoice(tenantId: string, invoiceId: string) {
return this.api.accountingApi.getInvoice(tenantId, invoiceId);
}
}
Webhook Configuration
// packages/workbooks-app-store/src/sync/webhooks/xero-webhook.ts
// Xero webhook payload verification
const signature = req.header("x-xero-signature");
const isValid = verifyXeroSignature(body, signature, webhookKey);
// Event types
// - CREATE, UPDATE, DELETE
// - Entity types: CONTACT, INVOICE, PAYMENT, etc.
Sync Process
1
Initial Full Sync
After connection, sync all historical data:
// Fetch all entities with pagination
const customers = await fetchAllPages(provider.getCustomers);
const invoices = await fetchAllPages(provider.getInvoices);
const items = await fetchAllPages(provider.getItems);
// Upsert to Taskr database
for (const customer of customers) {
await upsertCustomerFromAccounting(db, teamId, customer);
}
2
Delta Sync (Scheduled)
Daily sync fetches changes since last sync:
// Use modifiedSince parameter
const changes = await provider.getCustomers({
modifiedSince: lastSyncTimestamp,
});
// Process only changed records
for (const change of changes) {
await reconcileCustomer(db, teamId, change);
}
3
Real-time Webhook Sync
Webhooks provide immediate updates:
// packages/workbooks-app-store/src/sync/webhooks/quickbooks-webhook.ts
export async function handleQBOWebhook(event: QBOWebhookEvent) {
for (const entity of event.eventNotifications) {
const { name, id, operation } = entity.dataChangeEvent.entities[0];
switch (operation) {
case "Create":
case "Update":
await queueSync({ entityType: name, entityId: id, teamId });
break;
case "Delete":
await markDeleted(db, name, id);
break;
}
}
}
4
Push Changes from Taskr
Changes in Taskr trigger push sync:
// After creating/updating customer in Taskr
await pushToAccounting(db, {
entityType: "customer",
entityId: newCustomer.id,
teamId,
});
Field Mapping
Customer → QuickBooks Customer
const qboCustomer = {
DisplayName: customer.name,
CompanyName: customer.companyName,
PrimaryEmailAddr: { Address: customer.email },
PrimaryPhone: { FreeFormNumber: customer.phone },
BillAddr: {
Line1: customer.billingAddress.line1,
City: customer.billingAddress.city,
CountrySubDivisionCode: customer.billingAddress.state,
PostalCode: customer.billingAddress.postalCode,
Country: customer.billingAddress.country,
},
Notes: customer.notes,
Active: customer.status === "active",
};
Invoice → Xero Invoice
const xeroInvoice = {
Type: "ACCREC",
Contact: { ContactID: customer.xeroContactId },
Date: invoice.date,
DueDate: invoice.dueDate,
Reference: invoice.number,
Status: mapInvoiceStatus(invoice.status),
LineItems: invoice.lineItems.map(item => ({
Description: item.description,
Quantity: item.quantity,
UnitAmount: item.unitPrice,
AccountCode: item.accountCode,
TaxType: item.taxCode,
})),
CurrencyCode: invoice.currency,
};
Conflict Resolution
Conflict Handling
async function resolveConflict(
workbooksEntity: Entity,
accountingEntity: Entity
): Promise<"workbooks" | "accounting" | "conflict"> {
const wbUpdated = new Date(workbooksEntity.updatedAt);
const accUpdated = new Date(accountingEntity.updatedAt);
const timeDiff = Math.abs(wbUpdated.getTime() - accUpdated.getTime());
// If updates within 5 seconds, flag as conflict
if (timeDiff < 5000) {
await logConflict(workbooksEntity, accountingEntity);
return "conflict";
}
return wbUpdated > accUpdated ? "workbooks" : "accounting";
}
Error Handling
Token Expired
Token Expired
Cause: OAuth refresh token has expiredRecovery:
try {
await provider.refreshToken();
} catch (error) {
if (error.code === "invalid_grant") {
await markConnectionNeedsReauth(connectionId);
await notifyTeamOwner("Please reconnect your accounting software");
}
}
Rate Limited
Rate Limited
QuickBooks: 500 requests/minute
Xero: 60 requests/minuteRecovery:
if (error.statusCode === 429) {
const retryAfter = error.headers["retry-after"] || 60;
await delay(retryAfter * 1000);
return retry();
}
Validation Error
Validation Error
Cause: Data doesn’t meet accounting system requirementsRecovery:
if (error.type === "ValidationFault") {
for (const fault of error.faults) {
await logSyncError({
entityId,
field: fault.element,
message: fault.message,
});
}
// Skip entity, continue with others
}
Sync Token Stale
Sync Token Stale
Cause: QuickBooks entity was modified externallyRecovery:
if (error.code === "StaleObject") {
// Refetch current version
const current = await provider.getEntity(entityId);
// Merge changes and retry
const merged = mergeChanges(localEntity, current);
return provider.updateEntity(merged);
}
Database Schema
// Accounting Connections
export const accountingConnections = pgTable("accounting_connections", {
id: uuid("id").primaryKey().defaultRandom(),
teamId: uuid("team_id").references(() => teams.id),
provider: text("provider").notNull(), // quickbooks, xero
accessToken: text("access_token").notNull(), // encrypted
refreshToken: text("refresh_token").notNull(), // encrypted
tokenExpiresAt: timestamp("token_expires_at"),
// Provider-specific IDs
realmId: text("realm_id"), // QuickBooks
tenantId: text("tenant_id"), // Xero
// Sync state
lastSyncAt: timestamp("last_sync_at"),
syncStatus: text("sync_status"), // idle, syncing, error
webhookId: text("webhook_id"),
});
// External Entity References
export const accountingSyncMap = pgTable("accounting_sync_map", {
id: uuid("id").primaryKey().defaultRandom(),
connectionId: uuid("connection_id").references(() => accountingConnections.id),
entityType: text("entity_type").notNull(), // customer, invoice, etc.
workbooksId: uuid("workbooks_id").notNull(),
externalId: text("external_id").notNull(),
syncToken: text("sync_token"), // For QuickBooks
lastSyncedAt: timestamp("last_synced_at"),
syncDirection: text("sync_direction"), // push, pull, bidirectional
});
Webhook Security
// QuickBooks webhook verification
export function verifyQBOWebhook(payload: string, signature: string): boolean {
const hmac = crypto.createHmac("sha256", env.QBO_WEBHOOK_TOKEN);
hmac.update(payload);
const hash = hmac.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(signature)
);
}
// Xero webhook verification
export function verifyXeroWebhook(payload: string, signature: string): boolean {
const hmac = crypto.createHmac("sha256", env.XERO_WEBHOOK_KEY);
hmac.update(payload);
const hash = hmac.digest("base64");
return hash === signature;
}
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
accounting.connect | Mutation | Initiate OAuth flow |
accounting.disconnect | Mutation | Remove connection |
accounting.sync | Mutation | Trigger manual sync |
accounting.status | Query | Get sync status |
accounting.conflicts | Query | List unresolved conflicts |
accounting.resolveConflict | Mutation | Resolve a conflict |
Related Documentation
Bank Sync
Complement accounting sync with bank transactions
Invoice Payment
Track invoice payments and reconciliation