---
title: "Account Sources"
description: "Learn how to link and unlink external data sources to accounts via the API."
canonical: https://developer.billabex.com/en/guides/sources/
lang: en
alternate: https://developer.billabex.com/fr/guides/sources-de-compte/
last-updated: 2026-09-12
---

# Account Sources

> Learn how to link and unlink external data sources to accounts via the API.

Source: https://developer.billabex.com/en/guides/sources/
Language: English (en)
French version: https://developer.billabex.com/fr/guides/sources-de-compte/

When synchronizing accounts from an external system (ERP, CRM, billing platform), you can link each Billabex account to its corresponding record in your system using a **source reference**. This guide explains how sources work and how to manage them via the API.

## What is a Source?

A source is a reference that links a Billabex account to an external system. It tells Billabex:

- **Where** the account data comes from (your integration identifier)
- **Which** record it maps to (the external system ID)
- **When** it was last synchronized

When an account has a source, Billabex treats it as externally managed. This affects how the account can be modified and deleted.

### An account source and a document `sourceId` are two different things

An organization has no single data source. It can hold several connector connections at once (Odoo, Pennylane, Qonto, Sellsy, Stripe, Zoho Books) while its team keys invoices into the web app and your integration posts others through this API.

The source described on this page belongs to an **account**. Invoices and credit notes do not carry a source reference, but each one can carry a flat `sourceId`: **your** identifier for that document in **your** system. Same word, two namespaces, two scopes:

| Field             | Lives on               | Scope of uniqueness | Set by                                 |
| ----------------- | ---------------------- | ------------------- | -------------------------------------- |
| `source.sourceId` | an account             | per connection      | `PUT /accounts/{accountId}/source`     |
| `sourceId`        | an invoice/credit note | per organization    | `POST /invoices`, `POST /credit-notes` |

Send `sourceId` when you create a document and you get an idempotency handle for free: a second create with the same `sourceId` is rejected with `app-invoicing.invoice.duplicate-source-id` (or `app-invoicing.credit-note.duplicate-source-id`) as a `400`, instead of silently producing a duplicate after a lost HTTP response. Recover the document with `GET /invoices?filters=sourceId:is:YOUR-ID`. The value is frozen at creation, never editable afterwards, opaque to Billabex, and **case-sensitive**: `sourceId` only accepts the `is`, `isAnyOf`, `isEmpty` and `isNotEmpty` filter operators, never `contains` or `equals`.

Document numbers are unique **per account**, not per organization: two of your customers may each have their own `AVKA0011527`, and both are accepted. A lookup by number scoped to an account answers that account's document; a lookup by number across a whole organization may answer any one of the namesakes, so pass the account whenever you know it. Reconciling on `sourceId` avoids the question entirely.

Consequences worth internalizing:

- **An account without a source is perfectly normal.** It simply means no connector drives it. It is not an incomplete or broken record.
- **The source of an account never tells you where one of its invoices came from.** Every invoice and credit note carries its own `channel`, set once at creation and never changed afterwards: `PublicApi` when created through this API, `Manual` when typed in the web app, `AdminApi` or `Mcp` for the other programmatic entry points, or the connector name when imported by a connector. Documents created before these entry points were told apart carry the historical `Api` value. Read `channel` on the document, not `source` on the account.
- **A connector-imported document also carries a `connectionId`**, naming the exact connection it came from. It is `null` on everything you create, and on documents that predate the field.
- **Never derive "can I write this" from `channel` or `source`.** Every account, invoice and credit note carries `readOnlyReason`: `null` means it accepts writes, otherwise it names the connector that would overwrite them. It is returned by the single-resource endpoints **and** by every item of `GET /accounts`, `GET /invoices` and `GET /credit-notes`, so paging a list already tells you which rows you may update or delete. A write attempted while it is set fails with `app-invoicing.document.managed-action-forbidden`.
- **`source: null` alone does not mean "never connected".** An account also carries `lastUnlinkedSource`, the source it was detached from. Both `null` means the account was created by hand or through this API; `source: null` with `lastUnlinkedSource` set means the link to a billing tool was broken, so the figures no longer follow that tool. Read the two together before treating an account as manually managed.

## Source Properties

A source reference contains four properties:

| Property        | Type     | Description                                                   |
| --------------- | -------- | ------------------------------------------------------------- |
| `connectionId`  | string   | Identifier for your integration (e.g., `my-erp-sync`)         |
| `sourceId`      | string   | The account's ID in your external system (e.g., `CUST-00123`) |
| `connectorType` | string   | Always `Custom` for API-managed sources                       |
| `lastUpdate`    | datetime | When the source was last linked or updated                    |

The `connectionId` groups accounts from the same integration. Use a consistent value across all accounts from the same source system.

The `sourceId` uniquely identifies the account in your external system. This should match the primary key or unique identifier in your source database.

## Linking a Source

Use the **PUT** endpoint to link a source to an existing account:

```
PUT /api/public/v1/accounts/{accountId}/source
```

This endpoint is **idempotent**: if you call it with the same source (matching `connectionId` and `sourceId`), it will simply update the `lastUpdate` date instead of returning an error.

### Request

```javascript
const response = await fetch(
  'https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source',
  {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      connectionId: 'my-erp-integration',
      sourceId: 'CUST-00123',
      // Optional: specify a custom lastUpdate date
      // lastUpdate: '2024-01-15T10:30:00.000Z',
    }),
  },
);

const account = await response.json();
```

### Using cURL

```bash
curl -X PUT "https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "my-erp-integration",
    "sourceId": "CUST-00123"
  }'
```

### Request Body

| Property       | Type     | Required | Description                                             |
| -------------- | -------- | -------- | ------------------------------------------------------- |
| `connectionId` | string   | Yes      | Identifier for your integration                         |
| `sourceId`     | string   | Yes      | The account's ID in your external system                |
| `lastUpdate`   | datetime | No       | When the source was last synchronized (defaults to now) |

### Response

The endpoint returns the updated account with the source reference:

```json
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "fullName": "Acme Corporation",
  "source": {
    "connectionId": "my-erp-integration",
    "sourceId": "CUST-00123",
    "connectorType": "Custom",
    "lastUpdate": "2024-01-15T10:30:00.000Z"
  }
}
```

Note that `connectorType` is always set to `Custom`. If `lastUpdate` is not provided, it defaults to the current server time.

## Updating the Synchronization Date

Use the **PATCH** endpoint to update only the `lastUpdate` date of an existing source:

```
PATCH /api/public/v1/accounts/{accountId}/source
```

This is useful when you want to mark an account as "recently synchronized" without re-linking the entire source.

### Request

```javascript
const response = await fetch(
  'https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source',
  {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      // Optional: specify a custom lastUpdate date
      // If omitted, defaults to the current time
      lastUpdate: '2024-01-15T10:30:00.000Z',
    }),
  },
);

const account = await response.json();
```

### Using cURL

```bash
# Update with current timestamp
curl -X PATCH "https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

# Update with specific timestamp
curl -X PATCH "https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "lastUpdate": "2024-01-15T10:30:00.000Z"
  }'
```

### Request Body

| Property     | Type     | Required | Description                                             |
| ------------ | -------- | -------- | ------------------------------------------------------- |
| `lastUpdate` | datetime | No       | The new synchronization date (defaults to current time) |

### Response

The endpoint returns the updated account:

```json
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "fullName": "Acme Corporation",
  "source": {
    "connectionId": "my-erp-integration",
    "sourceId": "CUST-00123",
    "connectorType": "Custom",
    "lastUpdate": "2024-01-15T10:30:00.000Z"
  }
}
```

> **Note**: This endpoint only works for accounts that already have a source linked. If the account has no source, you'll receive a `400 Bad Request` error.

## Creating an Account with Source

You can also include the source when creating a new account:

```javascript
const response = await fetch('https://next.billabex.com/api/public/v1/accounts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    organizationId: 'YOUR_ORG_ID',
    fullName: 'Acme Corporation',
    currencyCode: 'EUR',
    billingAddress: {
      street: '123 Main Street',
      city: 'Paris',
      postalCode: '75001',
      country: 'FR',
    },
    source: {
      connectionId: 'my-erp-integration',
      sourceId: 'CUST-00123',
    },
  }),
});
```

This creates the account and links the source in a single request.

## Unlinking a Source

To remove the source link from an account, use the **DELETE** endpoint:

```
DELETE /api/public/v1/accounts/{accountId}/source
```

### Request

```javascript
const response = await fetch(
  'https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source',
  {
    method: 'DELETE',
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
);

// Returns 204 No Content on success
```

### Using cURL

```bash
curl -X DELETE "https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

The endpoint returns `204 No Content` on success.

## Implications in Billabex

Linking a source to an account has several important implications:

### 1. Deletion Protection

Accounts with a source cannot be deleted through the Billabex UI. Users must first unlink the source before the account can be removed. This prevents accidental deletion of synchronized data.

### 2. UI Exclusion Disabled

Accounts with a `Custom` source cannot be excluded (blacklisted) through the Billabex UI. The exclusion feature is designed for connector-managed accounts where users might want to filter which accounts are synced. Odoo v1 intentionally synchronizes every account and does not expose this filter yet.

For API-managed accounts, you control which accounts exist in Billabex directly through your integration.

### 3. Orphan Status After Unlink

When you unlink a source, the account becomes an "orphan" - it no longer has a connection to any external system. The account remains in Billabex and can then be:

- Deleted manually through the UI
- Re-linked to a different source via the API
- Managed independently

### 4. Only Custom Sources Can Be Unlinked via API

The unlink endpoint only works for accounts with a `Custom` source. Accounts linked through Billabex connectors (Odoo, Pennylane, Qonto, Sellsy, Stripe, Zoho Books) must be managed through their respective connection settings.

## Best Practices

### Use Consistent Connection IDs

Choose a meaningful `connectionId` that identifies your integration:

```javascript
// Good - identifies the integration clearly
connectionId: 'salesforce-billing-sync';
connectionId: 'netsuite-production';

// Avoid - too generic or inconsistent
connectionId: 'sync';
connectionId: 'integration-1';
```

### Use Stable Source IDs

The `sourceId` should be the primary key or stable identifier from your system:

```javascript
// Good - stable identifiers
sourceId: 'CUST-00123'; // Customer number
sourceId: 'acc_1234567890'; // Database ID

// Avoid - values that might change
sourceId: 'acme-corp'; // Company name (can change)
```

### Link Before Creating Related Data

When importing data, link the source first, then create invoices and contacts:

1. Create or update the account with source
2. Create contacts
3. Create invoices referencing the account

This ensures all data is properly associated with the external system reference.

### Handle Re-linking Gracefully

The link endpoint (PUT) is idempotent. If you call it with the same `connectionId` and `sourceId`:

- The `lastUpdate` date will be updated
- No error will be returned

If you need to change the source to a different external record, you must first unlink the current source, then link the new one:

```javascript
// 1. Unlink current source
await fetch('https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${accessToken}` },
});

// 2. Link new source
await fetch('https://next.billabex.com/api/public/v1/accounts/ACCOUNT_ID/source', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    connectionId: 'new-erp-integration',
    sourceId: 'NEW-CUST-456',
  }),
});
```

## Next Steps

- **[Upsert Operations](/en/guides/upsert/)** : Create or update contacts in a single request
- **[Contacts](/en/guides/contacts/)** : Manage people associated with accounts
- **[Getting Started](/en/guides/getting-started/)** : API basics and authentication
- **[API Reference](/en/api-reference/)** : Full endpoint documentation

## Support

Questions about account sources?  
Contact us via the [website contact form](https://billabex.com).

---

Billabex developer portal. OpenAPI specification: https://developer.billabex.com/openapi.json.
Agent instructions: https://developer.billabex.com/llms.txt. Complete documentation: https://developer.billabex.com/llms-full.txt.
