Skip to main content

Customers and suppliers

Every invoice attaches a seller and a buyer; your workspace directory carries these parties as records: customers (customers) that a company in the workspace sells to, suppliers (suppliers) that bill it. Create the record once, then reference it by party_id on every invoice creation: Scribee copies its data onto the invoice, and your invoicing calls reduce to amounts and lines. The endpoints on this page cover creating, reading, updating, and deleting these records; none of them transmits anything to the PPF (Portail Public de Facturation), the Peppol network, or the party itself.

What Scribee does for you​

  • Normalizes identifiers at registration: vat_identifier, legal_registration_id, identifier, and endpoint_id are stripped of leading and trailing spaces then uppercased; the IBAN loses all its spaces.
  • Checks formats at write time: IBAN (format and check key), VAT number (general format and French format), consistency between legal_registration_id and the declared scheme (9 digits for a SIREN, 14 for a SIRET).
  • Applies French by default: communication_language and billing_language are set to fr on any record created without these fields.
  • Copies the record onto every invoice that references it by party_id: the invoice keeps its own copy of the data, unaffected by later changes to the record.

Two directories, one shape​

  • GET / POST /api/v1/workspaces/{workspace_id}/customers - list and create customers
  • GET / PATCH / DELETE /api/v1/customers/{id} - read, modify, delete a customer record
  • Same shape on the supplier side: GET / POST /api/v1/workspaces/{workspace_id}/suppliers and GET / PATCH / DELETE /api/v1/suppliers/{id}

Each record belongs to a company in the workspace: company_id designates it at creation (Companies and establishments). Without company_id, the company used is the one with the lowest id among the workspace's eligible companies - every company on the supplier side, only the companies holding the sales capability on the customer side (see Invisible customers). If no company is eligible, the call returns 404. The owning company is fixed at creation: see step 4.

category_id attaches the record to a category in your workspace (see Categories). The customer record carries four fields the supplier record does not: customer_type, first_name, and last_name (see step 2), as well as einvoice_format (see E-invoice format).

Identifier types​

Three field pairs carry the identifiers of a record: legal_registration_id + legal_registration_scheme_id (legal registration), identifier + identifier_scheme_id (additional identifier), and endpoint_id + endpoint_scheme_id. On write, the scheme field accepts the Scribee key (siret) or its equivalent code (0009); responses always return the key.

The three scheme fields accept the same set of keys, and that set goes well beyond the French identifiers: Scribee receives invoices from the whole network and must be able to record the scheme declared by a Belgian or Italian issuer as well as your own. The list of accepted keys is published as the enum of the legal_registration_scheme_id parameter in the API reference (List customers) - that is the authoritative list, and it follows the code. The table below covers only the identifiers a French integration sets in practice.

API keyAccepted codeLabelRequired legal_registration_id format
siren0002SIREN9 digits
siret0009SIRET14 digits
ridet0228RIDET6 to 10 digits
tahiti0229Tahiti6 to 9 digits
european_union0223European Union2 letters then 2 to 30 letters or digits
international0227International1 to 50 characters, no whitespace
routing_code0224Routing code14 digits
b2c0226B2C1 to 30 letters, digits, or hyphens
aife0225AIFE (Agence pour l'informatique financière de l'État)14 digits
it_partita_iva0211Partita IVA (Italy)IT then 11 digits
global_location_number0088Global Location Number13 digits
france_platform0238French administration platform14 digits

The last column only checks legal_registration_id. identifier and endpoint_id accept any value: their scheme is recorded, never checked against the value. The value is uppercased before the check, so you can send it in lowercase.

The format check does not cover every accepted scheme. The twelve schemes in the table above are checked, and they are not the only ones: 36 of the 91 keys in the enum carry a format, including the Peppol schemes for which the code list publishes one - be_en (0208), lei (0199) or iban (9918) for example. Under the others, legal_registration_id is recorded as sent: the absence of a 422 then does not confirm the identifier is well formed. nl_kvk (0106) is one of them even though the code list publishes a format, that list contradicting itself on this point.

A key or a code absent from the reference's enum returns 422: the message carried by details.base is then produced by the framework, in English and untranslated, not a Scribee validation message.

single_taxable_entity (0231) is the exception. This scheme designates the SIREN of the single taxable entity the seller of a received invoice belongs to, and it is not a scheme you may state: sent on legal_registration_scheme_id, identifier_scheme_id or endpoint_scheme_id, under its key or its code, it is refused with 422 and code set to validation_failed. details then carries the offending field, with the Scribee message "doit être un schéma que vous pouvez déclarer ; single_taxable_entity (0231) n'est lu que sur l'identifiant d'un vendeur reçu". The record is neither created nor modified. On your own sales invoices, Scribee emits this scheme itself, from the company's single taxable entity settings (Issue a sales invoice).

On receipt, the rule is the opposite: on an invoice received from a third party, a scheme code absent from that same list is dropped instead of failing the import - legal_registration_id and identifier are recorded as sent and their scheme field is null, with no import error and no refusal. The endpoint_id / endpoint_scheme_id pair is the exception on a CII invoice (Factur-X included): a recognised electronic-address scheme is recorded there as usual, along with its identifier. Only when that scheme is absent or unrecognised is the whole pair dropped and both fields null - the identifier is not kept there without its scheme.

The seller of a received invoice is one more exception: an identifier it declares under scheme 0231, the SIREN of the single taxable entity it belongs to, is never taken into identifier. That field carries the first of the other identifiers the invoice gives it, or null if there is none. 0231 is not in the published enum: the legal_registration_scheme_id filter refuses it with 400 like any value outside the list.

A fourth pair carries your own external reference: external_source and external_id. These are free-form strings, capped at 255 characters each, with no format validation and no matching against the table above. The rule is not symmetric: external_id requires external_source, but not the other way around - a record can carry an external_source with no external_id. The external_source + external_id pair must be unique within a company and a single directory (customers or suppliers): if you use it as an idempotency key, a second creation with the same pair returns 422, with external_id carrying "a déjà été pris" in details.

Payment terms​

payment_terms accepts one of eight values. The field has no default value: it stays empty until you set it.

payment_termsLabel
seven_days7 days
fifteen_days15 days
thirty_days30 days
thirty_days_end_of_month30 days end of month
forty_five_days45 days
forty_five_days_end_of_month45 days end of month
on_receiptOn receipt
customCustom

E-invoice format​

einvoice_format carries a customer's own e-invoice format. It replaces, for that customer only, the setting of the company that sells to it. The field accepts three values, or null:

einvoice_formatFormat
ublUBL XML
ciiCII XML
facturxFactur-X
nullNo format of its own: the customer follows its company's setting

null is the value of every record created without this field. The company setting defaults to UBL and is changed from the Scribee interface; the API does not expose it. The response carries the value set on the record, never the inherited value: a null means the customer follows its company, not that no format applies.

The field is read on every customer record. It is written, on create as on update, only when the e-invoice format choice is enabled for your workspace. This feature is not enabled by default: Scribee enables it workspace by workspace. Until it is, einvoice_format is ignored without an error - the response is 201 or 200 and the field keeps its value. Check the returned value rather than the status code. Once the feature is enabled, send null on update to have the customer follow its company's setting again; a value other than ubl, cii, or facturx returns 422, with einvoice_format in details. Supplier records do not carry this field.

Once the feature is enabled, the resolved format - the record's own, else the company setting - is the syntax of the invoice copy Scribee sends over the Peppol network to the customer's platform: UBL, CII, or Factur-X (a PDF embedding the CII). If the customer's platform does not publish that format in the Peppol directory, the copy is sent in UBL; until the feature is enabled, it is always sent in UBL. Sending invoices by email is not affected.

Accounting setup​

Four fields describe a record's accounting setup - the one that files your entries in the right ledger and against the right account. They are read and written on customers as well as on suppliers.

  • accounting_ledger_id designates the record's accounting ledger. It is null until you set it, and the ledger must belong to the same company as the record (Accounting ledgers).
  • auxiliary_account_number is the auxiliary account identifying the record in your accounting software. Free-form string, null by default.
  • offset_accounts carries the record's offset accounts.
  • accounting_setup_complete is read-only and derived: it is true only when the record carries an accounting_ledger_id, an auxiliary_account_number, and at least one offset account. Sent on write, it is ignored.

An offset account can appear 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), generating an accounting entry for a record that carries no offset account yet creates one on that record: the expense account for a supplier, the revenue account for a customer, stored with default: true and tax_rate at null. That setting is declared in two places: on the company's connection to its accounting software, and on the workspace. The connection's value wins, the workspace's own is only a floor: a workspace whose setting is blank still produces an offset account when the company's connection declares one. Both therefore have to be blank for nothing to be created. A record that already carries at least one offset account is never touched. So do not treat an accounting_setup_complete at false as a stable state, nor offset_accounts as an array only you fill: between two reads, with no call of your own, a record can gain an entry and the field can turn true. That creation only touches offset_accounts: it sets neither accounting_ledger_id nor auxiliary_account_number.

The account_number of an existing entry can change too, under an unchanged id. When the record's company is connected to an accounting software from the Scribee interface and the account set by default is corrected in that software, the next sync rewrites that entry instead of adding a second one: the entry keeps its id and its default, and its account_number takes the corrected value. That rewrite only targets the entry Scribee created for want of an offset account, and only while it is the record's only entry and nobody has changed its account_number. Correcting that number yourself therefore puts the entry beyond the sync's reach: your PATCH is not undone on the next pass, and the entry is treated like the ones you create. An entry you created, or one an earlier sync already reported, is never rewritten this way: a different number concerning it is added as a separate entry. So identify an offset account by its id, not by its account_number.

An offset_accounts entry carries seven fields:

{
"offset_accounts": [
{
"id": 71,
"account_number": "706000",
"category": "customers",
"cost_center": "CC-PARIS",
"default": true,
"product_type": "Prod_Functional_Costs",
"tax_rate": 20.0
}
]
}

account_number is required; category, cost_center, and product_type are free-form strings and are null by default. tax_rate is a JSON number or null, and only accepts the rates of the French catalogue: 0, 0.9, 1.05, 1.75, 2.1, 5.5, 7, 8.5, 9.2, 9.6, 10, 13, 19.6, 20, 20.6. null means the account targets no rate in particular. On read, the value comes back rounded to two decimals: 20 sent comes back as 20.0.

product_type carries the Product Type worktag the Workday accounting export writes for this account. Left at null, this account carries no value of its own: the value configured for the company applies instead. Set it only when this record needs its own, and send product_type: null on update to clear the value you set.

default is false by default and is never inferred: Scribee does not promote the first account of a rate, you set the value. A record carries at most one default: true account per tax_rate.

Writing the offset accounts​

At creation, every offset_accounts entry creates an account. account_number is required; send neither id nor _destroy, since the record has no account to look up yet - an id sent at creation returns 404, as for addresses and contacts.

On update, the array merges by id:

  • an entry with no id adds an account;
  • an entry with id modifies the matching account and leaves the omitted fields unchanged. Unlike addresses and contacts, no field has to be resent for the change to apply: { "id": 71, "default": false } is enough;
  • an entry with id and _destroy: true deletes the account;
  • accounts the array does not mention stay in place.
curl -X PATCH https://app.scribee.tech/api/v1/customers/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"accounting_ledger_id": 12,
"auxiliary_account_number": "411AUX001",
"offset_accounts": [
{
"account_number": "706000",
"category": "customers",
"tax_rate": 20,
"default": true
}
]
}
}'

Changing the default account of a rate​

The default: true account of a tax_rate is protected by a database uniqueness constraint, applied row by row at write time. Three ways to change that account, of which only one fails:

  • promoting an already-stored account in the same call as the demotion of the one in place fails, whatever the order of the entries in the array. The response is 422 with the code duplicate_record, and neither row is written;
  • creating a new default: true account in the same call as the demotion of the old one works, in both orders;
  • deleting the old one with _destroy: true while promoting an existing account in the same call works too.

Two successive PATCH calls - demote first, promote next - cover every case. That is the sequence to remember if you want a single rule.

{
"error": "unprocessable_entity",
"code": "duplicate_record",
"message": "La validation a échoué",
"details": {
"base": ["Un enregistrement avec cet identifiant existe déjà"]
}
}

Step 1: create a customer​

This call creates a record in the workspace directory. It transmits nothing externally and is undone with the DELETE from step 6. Only name is required; addresses and contacts are declared in the same call. Every field of the record goes under a wrapping key customer (supplier on the supplier side): a body sent without it returns 400.

curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/customers \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"name": "Acme Corp",
"vat_identifier": "FR32443061841",
"legal_registration_id": "44306184100025",
"legal_registration_scheme_id": "siret",
"payment_terms": "thirty_days",
"addresses": [
{
"label": "Siège social",
"address_line_1": "15 avenue des Champs-Élysées",
"postal_code": "75008",
"city": "Paris",
"country_code": "FR",
"address_type": "billing",
"default_billing": true
}
],
"contacts": [
{
"name": "Marie Martin",
"email": "marie.martin@example.com",
"default": true
}
]
}
}'

201 response, abridged to the fields useful here:

{
"data": {
"id": 12,
"type": "customer",
"company_id": 1,
"name": "Acme Corp",
"customer_type": "company",
"einvoice_format": null,
"vat_identifier": "FR32443061841",
"legal_registration_id": "44306184100025",
"legal_registration_scheme_id": "siret",
"payment_terms": "thirty_days",
"communication_language": "fr",
"billing_language": "fr",
"payment_methods": [],
"addresses": [
{
"id": 34,
"address_line_1": "15 avenue des Champs-Élysées",
"postal_code": "75008",
"city": "Paris",
"country_code": "FR",
"address_type": "billing",
"default_billing": true,
"default_delivery": false
}
],
"contacts": [
{
"id": 21,
"name": "Marie Martin",
"email": "marie.martin@example.com",
"phone": null,
"default": true
}
]
}
}

Note the id: it is the party_id your invoices reference.

No field of the record itself is conditional: every record, in a list as well as on its own, carries the full field set (id, type, company_id, category_id, name, trading_name, vat_identifier, the three identifier + scheme pairs, directory_routing_identifier, external_source, external_id, company_legal_form, iban, payment_terms, communication_language, billing_language, accounting_ledger_id, auxiliary_account_number, accounting_setup_complete, created_at, updated_at, payment_methods, addresses, contacts, offset_accounts, plus customer_type, first_name, last_name, and einvoice_format on a customer), outside of the include expansion (see step 3). The JSON extracts in the following steps are abridged the same way.

Among those fields, directory_routing_identifier carries the routing identifier picked out of the directory. It is readable on every record and writable on both record endpoints, at creation as well as on update; it is the value Scribee emits as BT-34 / BT-49 under scheme 0225 (National directory).

Nested collections​

  • addresses and contacts are JSON arrays of objects, payment_methods a JSON array of strings. A value of any other shape - a single object, a bare string - is discarded without an error: check the collection returned in the response.
  • An address requires address_line_1, postal_code, city, and a 2-character country_code. Only the length is checked, never the content: 12 or ?? are accepted just like FR. Send an uppercase ISO 3166-1 alpha-2 code - nothing in the API will catch a bogus value, and it comes back out unchanged in invoices and regulatory flows. address_type defaults to billing and accepts billing, delivery, or both. default_billing requires address_type billing or both, default_delivery requires delivery or both; a record carries at most one default_billing address and one default_delivery address. default_billing designates the address used when the record has more than one.
  • A contact requires name and at least one of email or phone. An email that is present must also be a well-formed address, otherwise the record is refused with a 422. A record carries at most one default contact.
  • An addresses entry whose address_line_1, city, postal_code, and country_code are all empty, or a contacts entry whose name, email, and phone are all empty, is discarded before validation: the response is 201 and the entry is simply absent. A partially filled entry, by contrast, returns 422.
  • Send no id in addresses or contacts at creation: the record has no elements to look up yet, and the call returns 404. Reposting the body of a GET response as-is triggers this case.

Payment methods​

payment_methods accepts five values: bank_transfer, check, direct_debit, cash, and card. An unrecognized value is discarded without an error - the response is 201 and the value is absent from the returned payment_methods. Check the returned array rather than the status code.

Step 2: create an individual customer (B2C)​

A customer is a company (customer_type: "company", the default value) or an individual (customer_type: "consumer"). For an individual, first_name and last_name are required and name is recomputed as "First name Last name"; the default contact's name is aligned with the record's. The b2c identifier scheme from the table above is meant to carry an individual's identifier.

Do not confuse customer_type with counterparty_type: the latter is not an API field. Sent on create or on update, on a customer as on a supplier, it is ignored without an error, and it appears in no response.

curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/customers \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"customer_type": "consumer",
"name": "Jean Dupont",
"first_name": "Jean",
"last_name": "Dupont"
}
}'
{
"data": {
"id": 13,
"type": "customer",
"customer_type": "consumer",
"name": "Jean Dupont",
"first_name": "Jean",
"last_name": "Dupont"
}
}

Step 3: list and look up a record​

GET /api/v1/workspaces/{workspace_id}/customers returns the workspace's records, paginated (page, per_page - 20 by default, 100 maximum) and sortable (sort_by: name, trading_name, vat_identifier, created_at, or updated_at; sort_order: asc or desc; default sort: name ascending). The data + meta envelope follows API conventions.

curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/customers?sort_by=created_at&sort_order=desc" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{
"id": 12,
"type": "customer",
"name": "Acme Corp",
"vat_identifier": "FR32443061841"
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 1,
"total_count": 1
}
}

The list accepts eight filter parameters, combinable with one another: company_ids (one or more company ids, for example ?company_ids[]=1&company_ids[]=2), vat_identifier, legal_registration_id, legal_registration_scheme_id, external_source, external_id, auxiliary_account_number, and accounting_setup_status. For the latter seven, a parameter left empty is ignored - it filters nothing, rather than matching no results. company_ids behaves differently: ?company_ids[]= with no value is sent as an array containing an empty string, which the API does not treat as an absent parameter - the filter then applies with that empty value, which matches no record, and the response is 200 with an empty list. vat_identifier and legal_registration_id are compared after the same normalization applied at write time (whitespace stripped, value uppercased); external_source, external_id, and auxiliary_account_number are compared without normalization: the submitted value is neither trimmed nor transformed. The comparison itself is the database's own, insensitive to case and to accents. legal_registration_scheme_id only accepts the keys published in the parameter's enum (Identifier types) - a value outside that list returns 400 (see 400 Bad Request). accounting_setup_status only accepts complete and incomplete: complete returns only the records carrying an accounting_ledger_id, an auxiliary_account_number, and at least one offset account (Accounting setup), incomplete returns every record missing at least one of the three pieces. Any other value returns 400. No filter by name, category, or date, and no full-text search: keep the id returned at creation to look up a record these eight filters cannot isolate on their own.

GET /api/v1/customers/{id} returns a single record - the path references the record directly, with no workspace_id: the identifier is resolved across every workspace attached to your OAuth client. On both the list and the single record, include=company adds the owning company (id, name, legal_identifier) to the response.

Step 4: update a record​

PATCH accepts the same fields as creation, all optional: send the fields you want to change. One exception: company_id is accepted then ignored. The owning company is fixed at creation, the response is 200 and the record is unchanged on that point; to move a record, delete it and recreate it under the target company.

In addresses and contacts:

  • an entry with no id adds an element;
  • an entry with id and _destroy: true deletes the element;
  • an entry with id modifies the element, provided it carries at least one of the fields that decide rejection: address_line_1, city, postal_code, or country_code for an address, name, email, or phone for a contact. An entry carrying none of them - for example { "id": 21, "default": true } - is discarded silently: the response is 200 and the element is unchanged. Resend one of those fields alongside the value you want to change for the update to apply;
  • an id that does not belong to this record returns 404, not 422.

payment_methods replaces the selection instead of adding to it: the list you send becomes the exact list, and omitted methods are turned off. Omit the field to keep the current selection, send [] to turn everything off. addresses and contacts, by contrast, merge by id.

accounting_ledger_id, auxiliary_account_number, and offset_accounts are written in the same call, with merge rules of their own: see Accounting setup.

curl -X PATCH https://app.scribee.tech/api/v1/customers/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"payment_terms": "on_receipt",
"contacts": [
{ "id": 21, "phone": "+33 1 42 68 53 00" }
]
}
}'

200 response with the updated record. Invoices already created do not change: each keeps the copy of the data taken at its creation. To have the party fill in the record itself, see Update invitations.

Step 5: suppliers, the same mechanics​

The supplier endpoints have the same shape, the same pagination, the same sorting, and the same fields, without customer_type, first_name, last_name, or einvoice_format. Two behavioral differences: suppliers escape the sales-capability filtering that applies to customers (Invisible customers), and their deletion has a failure mode of its own (step 6).

curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/suppliers \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"supplier": {
"name": "Tech Solutions SAS",
"vat_identifier": "FR32443061841",
"legal_registration_id": "44306184100025",
"legal_registration_scheme_id": "siret"
}
}'
{
"data": {
"id": 27,
"type": "supplier",
"name": "Tech Solutions SAS",
"vat_identifier": "FR32443061841",
"legal_registration_id": "44306184100025",
"legal_registration_scheme_id": "siret"
}
}

Step 6: delete a record​

This call deletes the record along with its addresses, contacts, supporting documents, update invitations, portal accesses, and accounting offset accounts - the deletion is permanent. Existing invoices are not modified: each keeps its copy of the data, only the reference to the record is cleared. A record referenced by invoices can therefore be deleted. On the supplier side, attached products are kept and lose their reference to the supplier.

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

204 response with no body. The required scope is destroy or write. Same path on the supplier side: DELETE /api/v1/suppliers/{id}.

Two references block the deletion, and they report it the same way: a 422 carrying the code dependent_records, whose message names the blocking dependency. That body carries no details key, unlike the validation 422 described further down.

A supplier referenced by a purchase order or a purchase request returns:

{
"error": "unprocessable_entity",
"code": "dependent_records",
"message": "Vous ne pouvez pas supprimer l'enregistrement parce que les commandes d'achat dépendants existent"
}

A customer carrying a GoCardless SEPA direct-debit mandate or a GoCardless billing request returns the same body, with the name of its own blocking dependency in the message. Branch on the code rather than on the text. Detach the record from the documents concerned, then replay the deletion; GoCardless mandates and billing requests are handled from the Scribee interface.

What happens next​

  • When an invoice is created (Issue a sales invoice), a parties entry carrying party_id is resolved against the directory: Scribee copies the record's name, legal registration, VAT number, billing address, and default contact onto the invoice. A field passed explicitly in the entry overrides the record's value, and billing_address_id designates the address to use when the record has more than one. Two limits worth knowing: party_id is resolved among the customers and suppliers of the invoice's company, not of the whole workspace - an id belonging to another company is not found, and invoice creation is then refused with a 422, not silently degraded. And an unknown billing_address_id, one belonging to another record, or one pointing at a delivery-only address is silently ignored: Scribee then falls back to the record's default_billing address, or failing that its oldest billing address. Check the address actually stored in the creation response.
  • A record's supporting documents (banking details, contract, Kbis extract) are deposited at POST /api/v1/parties/{party_id}/supporting_documents (Supporting documents).
  • To have the party fill in its own information, send it an update invitation (Update invitations).
  • The record can change outside your own calls. When its company is connected to an accounting software from the Scribee interface, that software's sync may rewrite the record's name, vat_identifier, legal_registration_id, legal_registration_scheme_id, auxiliary_account_number and offset accounts, and set accounting_ledger_id while it is still null; any write to the record itself moves updated_at. Only the fields the software carries are affected, the others are left untouched. That sync does not only rewrite the records you created: it also creates customers and suppliers you never posted, out of the connected software's own parties. A record created this way carries the software's auxiliary account, but not necessarily an offset account: it then comes back with an empty offset_accounts and accounting_setup_complete at false, and it is returned by the accounting_setup_status=incomplete filter. Reconciling your own directory against the list, or counting incomplete records, must therefore expect records that appeared with no call of your own. The API exposes neither the connected software nor the date of the last sync: re-read the record (GET /api/v1/customers/{id}) instead of treating your last PATCH as the current state.

Errors and edge cases​

422: validation failed​

A record with no name, a malformed identifier, or an individual customer with no first name returns 422, with the causes in details under one key per offending field - the field name as it appears in the API, with no prefix and no humanization:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"name": ["doit être rempli(e)"]
}
}

Other messages in the same format, each under the key of the field it concerns: vat_identifier carries "n'est pas un numéro de TVA valide" or "n'est pas un numéro de TVA français valide (FR + 2 caractères + 9 chiffres)"; legal_registration_id carries "ne correspond pas au format du type d'identifiant déclaré" (for example a legal_registration_id that is not 14 digits with the siret scheme); iban carries "n'est pas un IBAN valide" or "ne passe pas la vérification de clé IBAN"; first_name carries "doit être rempli(e)" on a consumer customer with no first name. Fix the named field and replay the call.

An error on an address or a contact is filed under a composite key: the collection name (party_addresses, party_contacts), a dot, then the offending field - base when the error is not about a specific field:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"party_addresses.city": ["doit être rempli(e)"],
"party_addresses.default_delivery": ["ne peut pas être définie sur une adresse de facturation uniquement"],
"party_contacts.base": ["doit avoir un email ou un numéro de téléphone"]
}
}

Neither the position of the offending entry in the addresses or contacts array nor its id is reported - only the field name is.

Offset accounts follow the same shape, under the offset_accounts prefix - the collection name as it appears in the API, not an internal name as for the two collections above. The accounting ledger, for its part, is filed under accounting_ledger:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"accounting_ledger": ["doit exister"],
"offset_accounts.account_number": ["doit être rempli(e)"],
"offset_accounts.tax_rate": ["n'est pas inclus(e) dans la liste"],
"offset_accounts.default": ["est déjà défini par défaut pour ce taux de TVA"]
}
}

accounting_ledger carries the message "doit exister" in all three cases: an unknown accounting_ledger_id, a ledger outside your workspace, and a ledger belonging to another company of your workspace. The three are deliberately indistinguishable: a distinct response would allow probing the existence of ledgers outside your scope.

offset_accounts.default reports two default: true accounts on the same tax_rate in the same call. Promoting an already-stored account, by contrast, is reported by a 422 with the code duplicate_record (Changing the default account of a rate).

A value outside the list on legal_registration_scheme_id, identifier_scheme_id, endpoint_scheme_id, payment_terms, customer_type, communication_language, billing_language, or addresses[].address_type also returns 422, but the message carried by details.base is produced by the framework, in English and untranslated. Only 0231 on the three scheme fields gets a Scribee message, under the field's name (Identifier types).

404 Not Found​

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

The causes, indistinguishable from one another in the response:

  • the record does not exist;
  • the record belongs to a workspace outside your application's scope;
  • on GET, PATCH, and DELETE /api/v1/customers/{id} or /api/v1/suppliers/{id}, the record belongs to a workspace whose IP allowlist refuses the calling address;
  • the company_id passed at creation is unknown in the workspace or, for a customer, lacks the sales capability;
  • company_id is omitted at creation and no company in the workspace is eligible;
  • the record is a customer attached to a company without the sales capability (see below);
  • an addresses, contacts, or offset_accounts entry carries an id that does not belong to this record, or carries an id while the call is a creation.

Check the identifier and the workspace attachment (Your first call).

403 Forbidden​

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

On endpoints 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". The /api/v1/customers/{id} and /api/v1/suppliers/{id} endpoints answer 404 in that second case, not 403.

The required scopes: write for POST and PATCH, destroy or write for DELETE. Both GETs require read - which is also what a token requested without scope carries. Request scope=read write to cover this whole page (Authentication).

400 Bad Request​

Four distinct causes:

  • on lists, requesting a page beyond the last page of a non-empty collection returns "Le numéro de page dépasse le nombre de pages disponibles" in the usual error envelope. Bound your requests with meta.total_pages;
  • on lists, a legal_registration_scheme_id value absent from the parameter's enum returns "Valeur legal_registration_scheme_id invalide. Valeurs autorisées : ..." in the usual error envelope, where the message then lists every accepted key. Branch on error, which carries bad_request, not on that enumeration: it grows with every scheme added;
  • on lists, an accounting_setup_status value other than complete or incomplete returns "Valeur accounting_setup_status invalide. Valeurs autorisées : complete, incomplete" in the same envelope;
  • on POST and PATCH, a request body without the wrapping customer or supplier key returns 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é".

401 Unauthorized​

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

Invisible customers​

In an accounting-firm workspace, a company whose subscribed offer does not include sales exposes none of its customers through the API - unless its legal form exempts it: a company whose company_legal_form is SCI or LMNP (case-insensitively) exposes its customers whatever the subscribed offer. No call says why:

  • GET /api/v1/workspaces/{workspace_id}/customers returns 200 with "data": [];
  • GET, PATCH, and DELETE /api/v1/customers/{id} return 404 on records that do exist;
  • POST /api/v1/workspaces/{workspace_id}/customers returns 404, whether company_id designates one of those companies or is omitted and no eligible company remains.

Suppliers in the same workspace keep answering normally: that asymmetry between the two directories is the only diagnostic available. The API exposes neither the workspace type nor the subscribed offer. The resolution is on the workspace side, which must subscribe that company to an offer including sales - unless the company's legal form already exempts it, in which case its customers are visible with no change of offer; fixing the identifier or the scopes changes nothing.