Skip to main content

API conventions

All Scribee API endpoints share the same conventions: a single response envelope, pagination, sorting, inclusion of related resources, and errors in a constant format. Learn them once on this page; they apply identically to every resource. All the requests below are reads (GET): you can run them as they are on your production account, they create no data and send nothing externally.

Each convention is illustrated on the invoice list, GET /api/v1/workspaces/{workspace_id}/invoices, with the token obtained in Authentication and the workspace_id identified in Your first call.

The response envelope​

Every list response contains a data array. It also carries a meta object describing pagination, except on four sub-collections that are not paginated and return data alone: an invoice's supporting documents (/api/v1/invoices/{id}/supporting_documents), an invoice's payments (/api/v1/invoices/{id}/payments), a party's supporting documents (/api/v1/parties/{id}/supporting_documents), and a party's update invitations (/api/v1/workspaces/{workspace_id}/parties/{party_id}/update_invitations). Those return the whole collection; page and per_page have no effect there.

curl https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/invoices \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response abridged to the fields that matter here - each invoice is returned in its complete serialization:

{
"data": [
{
"id": 12345,
"invoice_number": "INV-2024-00156",
"issue_date": "2024-12-15",
"direction": "sales",
"lifecycle_state": "deposited",
"lifecycle_status_code": "200",
"tax_inclusive_amount": 1770.0
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 8,
"total_count": 156
}
}

Single-resource endpoints (for example GET /api/v1/invoices/{id}) return the same object under the data key, without meta.

Pagination​

page (default: 1) and per_page (default: 20, maximum: 100) control the returned window.

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/invoices?page=2&per_page=50" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": ["..."],
"meta": {
"current_page": 2,
"per_page": 50,
"total_pages": 4,
"total_count": 156
}
}

Three behaviors to know:

  • A numeric per_page value outside the 1-100 range is clamped to the nearest bound: per_page=500 returns 100 items, without error. An unreadable per_page value - empty, non-numeric, or sent in array form (per_page[]=10) - falls back to the default, 20.
  • Always send an integer page greater than or equal to 1. page=0, a negative, empty, or non-numeric value, or one sent in array form (page[]=1), returns 400 in the usual error envelope:
{
"error": "bad_request",
"message": "Le numéro de page doit être un entier supérieur ou égal à 1"
}
  • Requesting a page beyond the last one also returns 400, with a distinct message, as soon as the collection is not empty:
{
"error": "bad_request",
"message": "Le numéro de page dépasse le nombre de pages disponibles"
}

To page through an entire collection, advance page up to and including meta.total_pages.

Sorting​

sort_by selects the field, sort_order the direction (asc or desc, default: desc).

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/invoices?sort_by=due_date&sort_order=asc" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{ "id": 12290, "invoice_number": "INV-2024-00098", "due_date": "2025-01-05" },
{ "id": 12345, "invoice_number": "INV-2024-00156", "due_date": "2025-01-15" }
],
"meta": { "current_page": 1, "per_page": 20, "total_pages": 8, "total_count": 156 }
}

Accepted fields on the invoice list: issue_date (default), due_date, invoice_number, tax_inclusive_amount, lifecycle_state, created_at. An unknown sort_by or sort_order value does not cause an error: sorting falls back to the default, issue_date descending.

Lists return each invoice in the same complete serialization as the single-resource endpoint - parties, totals, references, and lifecycle information included. Only the associated collections are loaded on request, via the include parameter (comma-separated values).

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/invoices?include=lines" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{
"id": 12345,
"invoice_number": "INV-2024-00156",
"lines": [
{
"id": 501,
"line_id": "1",
"quantity": 2.0,
"line_extension_amount": 1475.0,
"item": { "name": "Prestation de conseil" }
}
]
}
],
"meta": { "current_page": 1, "per_page": 20, "total_pages": 8, "total_count": 156 }
}

Accepted values on invoices: lines, payment_means, tax_subtotals, lifecycle_events, allowance_charges, item_notes, invoice_references, payments, early_payment_discounts. An unknown value is ignored without error. Each endpoint lists its allowed values on its reference page, for example List invoices.

early_payment_discounts depends on an option enabled workspace by workspace. When it is not enabled, the include is still accepted but always returns an empty array, and the invoice's early_payment_discounts_extracted_at field is null: an empty array alone therefore does not mean no early-payment discount exists. Ask your Scribee contact whether the option is active on your workspace.

Errors​

Every error carries an HTTP status and a JSON body with a stable error key, intended for your processing logic, and a readable message, intended for your logs. Base your logic on the HTTP status and on error, never on the text of message. The messages are returned in French.

StatuserrorCauseWhat you do
400bad_requestmissing or incomplete request body, invalid page value, page beyond the last onefix the request before replaying it
401empty bodytoken missing, invalid, or expiredrequest a new token at /oauth/token and replay the request
403forbiddeninsufficient scope, insufficient rights on the resource, workspace not authorized for your application or non-existent, or IP address outside the allowlistcheck the token scopes; for workspace or IP access, contact your Scribee contact
404not_foundthe resource does not exist, or belongs to a workspace your application does not have access tocheck the identifier and the workspace queried
422unprocessable_entitya write payload failed validationfix the fields listed in details

400: three causes, one format​

{
"error": "bad_request",
"message": "Le corps de la requête est manquant ou mal formé"
}

Three causes share this status and this error key, and the message tells them apart. The message above signals a request body that is absent, or that does not carry the wrapping key the endpoint expects - {"customer": {...}} on customer creation, for example. The other two causes concern pagination and are detailed above: an invalid page value, and a page requested beyond the last one. In all three cases, fix the request before replaying it.

401: no body​

The 401 response has no JSON body; the detail is carried by the WWW-Authenticate header. Request a new token as described in Authentication.

403: five causes, one format​

{
"error": "forbidden",
"message": "L'application n'a pas accès à cet espace de travail"
}

Two message values cover three of these causes. The first (above) answers both for a workspace outside your application's scope and for a workspace_id matching no workspace: the response is the same word for word in both cases, so it never tells you whether the workspace exists. The second reports an IP address denied by the workspace's allowlist (Cette adresse IP n'est pas autorisée pour cet espace de travail). The two remaining causes - insufficient scope and insufficient rights on the target resource - share the same generic message, Vous n'êtes pas autorisé à effectuer cette action, and so cannot be told apart by the text. No 403 exposes an internal class name or technical wording: the messages stay in French, as everywhere else on the API. Base your logic on the 403 status and the error key. Scopes are fixed by requesting a new token; workspace and IP access are configured by Scribee, on request to your contact.

A workspace refusal does not surface the same way everywhere, and it is the shape of the endpoint that decides, never the reason for the refusal. On an endpoint that carries the workspace in its path, every workspace refusal is a 403: whether the workspace is outside your application's scope, does not exist, or has an IP allowlist that does not cover your source address. On an endpoint that addresses a resource directly by its identifier, there is no workspace_id to refuse: workspaces outside your scope, like those whose IP allowlist excludes you, are filtered out before the lookup and you receive a 404. So an unexpected 404 from a new server is first of all an IP question.

404: existence is not revealed​

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

A resource located in a workspace your application does not have access to returns 404, not 403: the API does not reveal the existence of resources outside your scope. An unexpected 404 on a known identifier therefore signals either a typo or missing workspace access.

422: the machine code and the field-level detail​

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"direction": ["doit être 'sales' ou 'purchases'"]
}
}

code is a stable snake_case machine identifier, never translated and never reworded. It names the failure class, where message is French text meant for your logs and details a map of the faulty fields: code is therefore what you branch on when the 422 status alone is not enough. error, message, and details keep the meaning and the format they had.

codeWhat it signals
validation_faileda validation refused the write; details names the fields
operation_failedthe operation was refused with no more specific cause
duplicate_recorda record with this identifier already exists
value_too_longa value exceeds the maximum length of its field
invalid_argumenta submitted value is not among the accepted values
dependent_recordsrecords linked to the resource block the operation

The catalogue may gain new values without notice; treat a code you do not recognize as a generic failure rather than rejecting the response.

details maps each faulty field to the list of its errors when the failure is attributable to a field. When it is not - a rule bearing on the whole request, a conflict with a linked resource - the messages are grouped under the base key. Some failures return a specific message without details: treat details as optional, and do not expect one key per submitted field.

422: a deletion blocked by linked records​

Deleting a company, an establishment, a customer, or a supplier that is still referenced elsewhere is refused with a 422 carrying the code dependent_records. When the API can identify the blocking dependency, it names it in the message and sends no details:

{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "Impossible de supprimer une entreprise qui a des filiales"
}

When it cannot, the refusal comes from the database and takes the generic shape, with the detail under details.base:

{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "La validation a échoué",
"details": {
"base": ["Cette opération est en conflit avec d'autres enregistrements liés à cette ressource"]
}
}

The dependent_records code is identical in both cases: branch on it rather than on the text. Detach or delete the linked records, then replay the deletion. Some dependencies can only be handled from the Scribee interface; each resource's page states which.