Skip to main content

Accounting ledgers

An accounting ledger (accounting_ledger) carries the code your accounting software files entries under: VE for sales, HA for purchases, BQ for the bank. Each ledger belongs to one company of the workspace and is visible only to it. Customer and supplier records attach to it through accounting_ledger_id (Customers and suppliers): that attachment is what makes a record's accounting setup usable. The endpoints on this page only touch your workspace's data and send nothing externally - neither to the PPF (Portail Public de Facturation) nor to Peppol.

What Scribee does for you​

  • Enforces uniqueness of name and code within a single company: two companies of the workspace may carry the same ledger code, one company may not.
  • Fixes the ledger to its company: company_id is required at creation then ignored on update, and a ledger never moves to another company.
  • Refuses to delete a ledger that is still referenced, rather than silently breaking the attachments in place.
  • Sorts the list by code ascending, with no sort parameter to supply.

The endpoints​

  • GET / POST /api/v1/workspaces/{workspace_id}/accounting_ledgers - list and create ledgers
  • GET / PATCH / DELETE /api/v1/accounting_ledgers/{id} - read, modify, delete a ledger

As with categories (Categories), only the list and the creation are addressed by workspace. GET, PATCH, and DELETE /api/v1/accounting_ledgers/{id} have no workspace_id in the path: the identifier is resolved across every workspace attached to your OAuth client.

A ledger carries six fields, all present in every response: id, company_id, code, name, created_at, and updated_at. There is no include parameter on this resource.

Step 1: create a ledger​

This call creates a ledger in your production workspace - there is no sandbox. It sends nothing externally and is undone with the DELETE from step 5. Every field goes under a wrapping key accounting_ledger: a body sent without it returns 400. The required scope is write: read is demanded on none of this page's writes.

name, code, and company_id are the three expected fields. name is capped at 255 characters, code at 50.

curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/accounting_ledgers \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accounting_ledger": {
"name": "Ventes",
"code": "VE",
"company_id": 1
}
}'

201 response:

{
"data": {
"id": 12,
"company_id": 1,
"code": "VE",
"name": "Ventes",
"created_at": "2026-07-31T11:15:00+02:00",
"updated_at": "2026-07-31T11:15:00+02:00"
}
}

Timestamps are serialized in ISO 8601 with the Europe/Paris offset (+02:00 in summer time, +01:00 in winter time).

company_id designates a company of the workspace in the path (Companies and establishments). Omitted, it returns 422; if it designates a company of another workspace, the call returns 404. Unlike customer and supplier records, there is no default company.

Step 2: list and look up a ledger​

GET /api/v1/workspaces/{workspace_id}/accounting_ledgers returns the ledgers of every company in the workspace, sorted by code ascending and paginated (page, per_page - 20 by default, 100 maximum). The read scope is enough. The data + meta envelope follows API conventions.

One filter is available: company_id, which narrows the list to a single company. Carrying a non-empty value, it is never ignored - a value designating no company of the workspace returns an empty list with 200, not the full list and not an error. Sent empty (?company_id=), by contrast, it is ignored: the response then carries every ledger of the workspace, with no error at all. That is the shape an unsubstituted template variable produces, and it silently widens the result instead of narrowing it - omit the parameter rather than sending it empty. There is no sort_by, no sort_order, and no search by name or code.

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/accounting_ledgers?company_id=1" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{
"id": 12,
"company_id": 1,
"code": "VE",
"name": "Ventes",
"created_at": "2026-07-31T11:15:00+02:00",
"updated_at": "2026-07-31T11:15:00+02:00"
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 1,
"total_count": 1
}
}

A single ledger is read at GET /api/v1/accounting_ledgers/{id}.

Step 3: attach a party to a ledger​

The attachment goes through the record's endpoints, not the ledger's: accounting_ledger_id is accepted on creating and updating customers and suppliers. The ledger must belong to the same company as the record - a ledger of another company, even in the same workspace, is refused with a 422 carrying the message "doit exister", indistinguishable from the response to a nonexistent id.

curl -X PATCH https://app.scribee.tech/api/v1/customers/42 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"accounting_ledger_id": 12,
"auxiliary_account_number": "411AUX001"
}
}'

The ledger is only one of the three pieces of a record's accounting setup; the other two are auxiliary_account_number and the offset accounts (Customers and suppliers).

Step 4: rename or recode​

PATCH covers name and code, both optional: send the fields you want to change. company_id is accepted then ignored - a ledger never moves to another company. The required scope is write.

curl -X PATCH https://app.scribee.tech/api/v1/accounting_ledgers/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accounting_ledger": {
"name": "Ventes France",
"code": "VE1"
}
}'

200 response with the updated ledger. Records already attached keep their accounting_ledger_id: renaming or recoding a ledger detaches nothing.

Step 5: delete a ledger​

DELETE is a permanent deletion, not an archive. The required scope is destroy or write: either one is enough, so a token carrying only write deletes as well.

curl -X DELETE https://app.scribee.tech/api/v1/accounting_ledgers/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

204 response with no body. The deletion is refused while any record still points at the ledger - a customer or supplier record, a bank account, or a posted accounting entry. Detach those records first, then replay the deletion.

What happens next​

  • Nothing goes to the PPF or the Peppol network: creating, modifying, or deleting a ledger touches no regulatory document and no invoice already issued.
  • The accounting_setup_complete of the attached records follows the attachment: it turns true only once the ledger, the auxiliary account, and at least one offset account are set (Customers and suppliers).
  • The accounting_setup_complete can also turn true outside your own calls. When a default expense account (compte de charge) or revenue account (compte de produit) is declared from the Scribee interface (that setting is not set through the API, and it is declared either on the company's connection to its accounting software, which wins, or on the workspace, which is only a floor), generating an accounting entry for a record that carries no offset account creates one on that record, which completes its setup with no call from you. So do not rely on an accounting_setup_complete at false as a stable state (Customers and suppliers).
  • The record lists accept the accounting_setup_status filter to find the ones whose setup is still incomplete.
  • A ledger can appear or change outside your own calls. When its company is connected to an accounting software from the Scribee interface, that software's sync rewrites the name of the ledgers it matches and creates the ones it does not. It matches a ledger by its code, and failing that by the identifier that same software had assigned to it during an earlier sync: a ledger recoded on the software side is therefore matched through that identifier and has its name rewritten, while its code stays as it was - the code of an existing ledger is never modified, this case included, so the ledger keeps a code the software no longer uses. Creation, for its part, is not unconditional: a reported code differing only by case (bq against BQ) from a code already stored in the same company is refused, reported as an error by the sync, and the corresponding ledger never appears. Since name stays unique within the company, a label already held by another ledger of the same company is stored followed by the code in parentheses: a second ledger labelled Banque, with code BQ10, is stored as Banque (BQ10). If that form is taken as well, a number is added inside the parentheses, starting at 2: Banque (BQ10 2), then Banque (BQ10 3), and so on up to 99; past that, the ledger is rejected and the sync reports it as an error. So do not rely on the exact shape of the stored label: the name you read back is not always the software's label - identify a ledger by its id or its code, not by its label.

Errors and edge cases​

400 Bad Request​

Three causes:

  • a request body without the wrapping accounting_ledger key, or with an empty accounting_ledger object, never reaches model validation: parameter reading fails first, and the response is a 400 in the usual {error, message} envelope, with error set to bad_request and the message "Le corps de la requête est manquant ou mal formé";
  • on the list, a page that is not an integer greater than or equal to 1: 0, a negative value, an empty value, a non-numeric value, or a page sent as an array or an object (page[]=1);
  • on the list, requesting a page beyond the last page of a non-empty collection.

A malformed page returns:

{
"error": "bad_request",
"message": "Le numéro de page doit être un entier supérieur ou égal à 1"
}

Going past the last page returns:

{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}

A malformed per_page produces no 400: it silently falls back to the default value.

422: validation failed​

Ledger validation errors are all grouped under details.base, with the code validation_failed. A name or a code already used by the same company:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Name a déjà été pris"]
}
}

The same body carries "Code a déjà été pris" when the code is the culprit, and "Name doit être rempli(e)" or "Code doit être rempli(e)" when the field is empty. A creation without company_id returns:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["L'entreprise est requise"]
}
}

Reuse the existing ledger (step 2) or pick another name / code pair.

422: ledger still referenced​

DELETE on a ledger a record still references returns a 422 carrying the code dependent_records. That body carries no details key, unlike the validation 422. An attached customer or supplier record returns:

{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "Impossible de supprimer un journal comptable avec des tiers associés."
}

A bank account or an accounting entry blocking the deletion returns the same code with a more general message:

{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "Échec de la suppression du journal comptable."
}

Branch on the code rather than on the text. Records are detached by sending accounting_ledger_id: null on the record; bank accounts and accounting entries are handled from the Scribee interface.

404 Not Found​

{
"error": "not_found",
"message": "La ressource demandée est introuvable"
}

The causes, indistinguishable from one another in the response:

  • the ledger does not exist;
  • the ledger belongs to a workspace outside your application's scope;
  • on GET, PATCH, and DELETE /api/v1/accounting_ledgers/{id}, the ledger belongs to a workspace whose IP allowlist refuses the calling address;
  • the company_id passed at creation does not designate a company of the workspace in the path.

403 Forbidden​

{
"error": "forbidden",
"message": "Vous n'êtes pas autorisé à effectuer cette action"
}

The required scopes: read for both GETs, write for POST and PATCH, destroy or write for DELETE. On the paths prefixed by {workspace_id}, a workspace not attached to your OAuth client returns "L'application n'a pas accès à cet espace de travail", and a workspace whose IP allowlist refuses the calling address returns "Cette adresse IP n'est pas autorisée pour cet espace de travail". On /api/v1/accounting_ledgers/{id}, that second refusal comes back as 404.

401 Unauthorized​

Token missing, expired, or revoked; the response body is empty. Request a new token (Authentication).