Categories
Your customers, your suppliers, and your invoices already carry a classification in your product: business lines, portfolios, expense types. Categories carry this taxonomy into Scribee: you create your labels at the workspace or company level, then attach them to your records and invoices via category_id. A category remains internal data: it is never emitted in any generated regulatory format (Factur-X, UBL, CII), and it is distinct from tax_category_id, the EN16931 VAT category code carried by invoice lines and subtotals (Issue a sales invoice). Every endpoint on this page touches only your workspace's data and sends nothing externally - not to the PPF (Portail Public de Facturation), not to Peppol.
What Scribee does for you
- Keeps your taxonomy out of regulatory documents: the Factur-X, UBL, and CII files Scribee generates never carry your
category_id. - Refuses an out-of-scope assignment: a category from another workspace or another company is rejected with a
422at write time. - Enforces name uniqueness among active categories in the same scope, case-insensitively; archiving a category frees up its name.
- Preserves your attachments on archiving: existing records and invoices keep their
category_id. - Protects your webhook subscriptions and your import filing: a category targeted by a webhook endpoint, or targeted by an import filing declaration, cannot be archived while that attachment exists.
Two scopes: workspace or company
A category carries an optional company_id. Without a company_id, it's a workspace template, assignable to the records and invoices of every company in the workspace (Companies and establishments). With a company_id, it's assignable only to the records and invoices of that company. In responses, tenant_id echoes the workspace_id from the path.
Category ids form a single namespace across every workspace linked to your application: GET, PATCH, and DELETE /api/v1/categories/{id} carry no workspace_id in the path and reach the category whichever workspace holds it, as long as that workspace is linked to your application. Only listing and creation are addressed per workspace.
On the list, scope=tenant filters workspace templates, scope=company filters company categories, and company_id targets a given company.
Step 1: create a category
This call creates a category in your production workspace - there is no sandbox. It sends nothing externally and everything is reversible: the name and color can be changed (step 4), archiving removes it from the lists (step 5). You can therefore run it as is, as a rehearsal. A read write token is required.
curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/categories \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"category": {
"name": "Grands comptes",
"color": "#3B82F6"
}
}'
201 response:
{
"data": {
"id": 12,
"name": "Grands comptes",
"color": "#3B82F6",
"tenant_id": 3,
"company_id": null,
"archived_at": null,
"created_at": "2026-07-31T11:15:00+02:00",
"updated_at": "2026-07-31T11:15:00+02:00"
}
}
Timestamps are serialized as ISO 8601 with the Europe/Paris offset (+02:00 in summer time, +01:00 in winter time). No field is returned in UTC with a Z suffix.
name is required, 255 characters maximum. color is optional, in the # followed by 6 hexadecimal characters format. Add company_id to the category object to create a company category; omit it for a workspace template.
Step 2: list and filter
GET .../categories returns the workspace's active categories, sorted by name ascending. The read scope is enough.
curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/categories?scope=tenant" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{
"id": 12,
"name": "Grands comptes",
"color": "#3B82F6",
"tenant_id": 3,
"company_id": null,
"archived_at": null,
"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
}
}
sort_by accepts name, created_at, updated_at, and archived_at; sort_order accepts asc and desc, and defaults to asc. An unrecognized value on either is ignored without an error and the default sort applies.
scope, company_id, and include_archived are case-sensitive, but they do not behave the same way. scope only accepts tenant and company; any other value is ignored, filters nothing, and returns the full list. include_archived adds archived categories only on the exact string true. company_id is never ignored: any non-blank value is applied as a filter exactly as sent. A company id outside this workspace, or one that contradicts scope, therefore returns an empty list with 200 - not the full list, and not an error.
Pagination follows the common conventions (API conventions): per_page defaults to 20 and is silently clamped to the 1-100 range, so per_page=500 returns 100 items with no warning. A single category is read at GET /api/v1/categories/{id} - a direct path, without workspace_id (Your first call); an archived category responds there too.
Step 3: assign a category
Assignment goes through the carrying resource's endpoints, not the category endpoints: category_id is accepted on creation and update of customers and suppliers (Customers and suppliers) as well as invoices (Issue a sales invoice). This call modifies a record in your directory and sends nothing externally; to revert, reassign a different category_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": {
"category_id": 12
}
}'
200 response, abridged to the fields useful here:
{
"data": {
"id": 42,
"type": "customer",
"company_id": 7,
"category_id": 12,
"name": "Acme Corp"
}
}
On read, every customer or supplier record and every invoice returns its category_id. Record and invoice lists do not expose a category filter: the attachment is read resource by resource.
On an invoice imported from a connected ERP, category_id may come back filled in without your having sent it. This filing does not hold for every connected ERP: it rests on a provenance code the invoice must carry at import, and a single connector transmits one today - ask Scribee whether your customer's is concerned before depending on it. Where it applies, your customer declares, from the Scribee interface, which category to apply according to where the invoice comes from in their ERP, and the declaration applies to invoices imported after it. Invoices already imported are not reclassified: saving the declaration does not pick them up, and neither does the ongoing synchronization - an explicit replay is required, requested from Scribee. This filing never goes over a category already attached - a category_id you send, like a category picked in the interface, always wins - and it never concerns an invoice created through the partner API. A missing declaration is not an error: the invoice simply arrives with no category, no message, and no import error.
A purchase invoice left with no category takes its supplier's: the category_id of the supplier record its seller is linked to. That link is made at creation when you send the supplier's party_id on the seller party, and, for a received invoice (Peppol, file upload, email), when Scribee recognizes the seller in the company's directory. A purchase invoice created through the API without a party_id is not matched against the directory and therefore does not receive this category. The inheritance only fills a blank: a category_id you send, a category picked in the interface or set by import filing always wins, then comes the category your customer associates, from the Scribee interface, with the Peppol address the invoice is received on; the supplier's comes last. It is inherited only if it is active and assignable to the invoice's company; otherwise the invoice stays without a category. Sales invoices are not concerned.
Step 4: rename or change the color
The update covers name and color; the scope (company_id) is fixed at creation - a company_id sent here is ignored without an error. A read write token is required.
curl -X PATCH https://app.scribee.tech/api/v1/categories/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"category": {
"name": "Comptes stratégiques"
}
}'
200 response with the updated category:
{
"data": {
"id": 12,
"name": "Comptes stratégiques",
"color": "#3B82F6",
"tenant_id": 3,
"company_id": null,
"archived_at": null,
"created_at": "2026-07-31T11:15:00+02:00",
"updated_at": "2026-07-31T14:02:00+02:00"
}
}
Step 5: archive
DELETE is an archive, not a deletion: it sets the archived_at timestamp and the category drops out of the default lists, but the record is kept. Records and invoices already attached keep their category_id, and the category remains readable by its id or with include_archived=true. A read write token is required.
curl -X DELETE https://app.scribee.tech/api/v1/categories/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
204 response, with no body. The call is idempotent: archiving an already archived category also responds 204. Archiving does not block writes: an archived category_id remains accepted on records and invoices. Reactivating an archived category is done from the Scribee interface.
First exception: a webhook endpoint targeted at the category blocks the archiving. The call then responds 422 with the code dependent_records (see below) and the category stays active. Unbind those endpoints - category_id to null - or delete them, then replay the DELETE (Webhooks). The block counts every endpoint of the workspace, but your writes only reach those of your application: an endpoint registered by another application or managed by the workspace is unbound from the Scribee interface, or by the application that owns it.
Second exception: a category targeted by an import filing declaration (step 3) blocks the archiving the same way, with the same 422 and the same dependent_records code; only the message differs (see below). That filing is not exposed by the API: remove the category from it, or point it at another category, from the Scribee interface, then replay the DELETE.
What happens next
- Nothing goes to the PPF or to the Peppol network: creating, assigning, or archiving a category touches no regulatory document, nor any already issued invoice.
- An archived category's name becomes available again: uniqueness only compares active categories in the same scope. You can therefore recreate an active category with the same name.
- Your reads return
category_ideverywhere it applies - customer and supplier records, invoices - with no extra parameter. - A category can also target a webhook endpoint, which then receives only the events of the invoices it carries (Webhooks). It is, along with import filing, one of the two attachments that hold back archiving.
Errors and edge cases
Category endpoints group all validation errors under details.base. An archiving refusal is the exception: it carries a code and no details, because it targets the whole category rather than a field.
400: empty category object
A body with no category object, or with an empty category object ({"category": {}}), never reaches model validation: parameter parsing fails first. The response is a 400 in the usual error envelope, with error set to bad_request and the message "Le corps de la requête est manquant ou mal formé":
{
"error": "bad_request",
"message": "Le corps de la requête est manquant ou mal formé"
}
Send at least name.
400: page out of range
On GET .../categories, a page higher than the number of available pages:
{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}
The meta.total_pages field of the previous response gives the bound.
422: missing name
name present but blank at creation:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Nom doit être rempli(e)"]
}
}
Provide a name and replay the call.
422: name already taken
An active category in the same scope already has this name - the comparison ignores case:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Nom a déjà été pris"]
}
}
Reuse the existing category (step 2) or choose a different name. A workspace template and a company category can carry the same name: the scope is part of the uniqueness rule.
422: invalid color
color outside the # + 6 hexadecimal characters format:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Couleur n'est pas valide"]
}
}
422: category_id refused on assignment
On the customer and supplier endpoints, every category_id the record cannot legitimately carry is refused under the category key:
{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"category": ["doit exister"]
}
}
category carries the message "doit exister" in all three cases: a category that does not exist, a category of another workspace, and a category of another company of your workspace. The three are deliberately indistinguishable: a distinct response would allow probing the existence of categories outside your scope, including those a member bound to a single company already does not see in their category picker. This is the normalization already applied to accounting_ledger_id on those same endpoints (Customers and suppliers).
The same scope rule applies to invoices. Check the category's company_id against that of the target record or invoice.
422: webhook endpoints bound
DELETE /api/v1/categories/{id} on a category a webhook endpoint targets:
{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "Des endpoints webhook sont encore rattachés à \"Grands comptes\". Détachez-les ou supprimez-les avant de l'archiver."
}
The message names the category, and this body carries no details: branch on code. List the workspace's webhook endpoints, set their category_id back to null or delete them, then replay the DELETE (Webhooks). The list covers the whole workspace while PATCH and DELETE only reach the endpoints of your application: those of another application or managed by the workspace respond 403 and are unbound from the Scribee interface. Attached records and invoices, for their part, never block archiving.
422: import filing bound
DELETE /api/v1/categories/{id} on a category targeted by an import filing declaration:
{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "Des correspondances d'import sont encore rattachées à \"Grands comptes\". Supprimez-les ou faites-les pointer vers une autre catégorie avant de l'archiver."
}
Same shape as the previous refusal - the message names the category, the body carries no details, branch on code. This setting is neither read nor changed through the API: correct it from the Scribee interface, in the integration settings of the company concerned.
404 Not Found
On GET, PATCH, and DELETE /api/v1/categories/{id}, three situations give the same response: the category does not exist, it belongs to a workspace not linked to your application, or it belongs to a linked workspace whose IP allowlist excludes the address the call comes from. These paths carry no workspace_id, so a scope refusal is indistinguishable from an unknown identifier. At creation, a company_id that does not designate a company in the workspace produces the same response:
{
"error": "not_found",
"message": "La ressource demandée est introuvable"
}
Check the identifier, the workspace attachment, and the calling IP address (Your first call).
403 Forbidden
Three causes, told apart only by the message:
- Insufficient scope. Reading requires a
readtoken; creation, update, and archiving require aread writetoken. Those two scope sets are the only ones that exist. Message:Vous n'êtes pas autorisé à effectuer cette action. - The
workspace_idin the path designates a workspace not linked to your application. Message:L'application n'a pas accès à cet espace de travail. - The path's workspace restricts IP addresses and the call does not come from an allowed one. Message:
Cette adresse IP n'est pas autorisée pour cet espace de travail.
The last two only occur on the paths that carry a workspace_id, that is, listing and creation. On /api/v1/categories/{id}, the same refusal appears as a 404.
{
"error": "forbidden",
"message": "Vous n'êtes pas autorisé à effectuer cette action"
}
Request a new token with the scopes you need (Authentication).
Related pages
- Customers and suppliers - the records that carry
category_id - Issue a sales invoice -
category_idon invoice creation, andtax_category_id, which it should not be confused with - Webhooks - targeting an endpoint at a category, and the archiving that targeting blocks
- API reference: list categories
- API reference: create a category
- API reference: archive a category