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
nameandcodewithin 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_idis 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
codeascending, with no sort parameter to supply.
The endpoints
GET/POST /api/v1/workspaces/{workspace_id}/accounting_ledgers- list and create ledgersGET/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_completeof the attached records follows the attachment: it turnstrueonly once the ledger, the auxiliary account, and at least one offset account are set (Customers and suppliers). - The
accounting_setup_completecan also turntrueoutside 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 anaccounting_setup_completeatfalseas a stable state (Customers and suppliers). - The record lists accept the
accounting_setup_statusfilter 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
nameof the ledgers it matches and creates the ones it does not. It matches a ledger by itscode, 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 itsnamerewritten, while itscodestays as it was - thecodeof 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 (bqagainstBQ) from a code already stored in the same company is refused, reported as an error by the sync, and the corresponding ledger never appears. Sincenamestays unique within the company, a label already held by another ledger of the same company is stored followed by thecodein parentheses: a second ledger labelledBanque, with codeBQ10, is stored asBanque (BQ10). If that form is taken as well, a number is added inside the parentheses, starting at 2:Banque (BQ10 2), thenBanque (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: thenameyou read back is not always the software's label - identify a ledger by itsidor itscode, not by its label.
Errors and edge cases
400 Bad Request
Three causes:
- a request body without the wrapping
accounting_ledgerkey, or with an emptyaccounting_ledgerobject, never reaches model validation: parameter reading fails first, and the response is a400in the usual{error, message}envelope, witherrorset tobad_requestand the message"Le corps de la requête est manquant ou mal formé"; - on the list, a
pagethat is not an integer greater than or equal to 1:0, a negative value, an empty value, a non-numeric value, or apagesent 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, andDELETE /api/v1/accounting_ledgers/{id}, the ledger belongs to a workspace whose IP allowlist refuses the calling address; - the
company_idpassed 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).
Related pages
- Customers and suppliers - the records that carry
accounting_ledger_id,auxiliary_account_number, and the offset accounts - Companies and establishments - the company a ledger belongs to
- API reference: list accounting ledgers
- API reference: create an accounting ledger
- API reference: retrieve an accounting ledger
- API reference: update an accounting ledger
- API reference: delete an accounting ledger