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, andendpoint_idare 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_idand the declared scheme (9 digits for a SIREN, 14 for a SIRET). - Applies French by default:
communication_languageandbilling_languageare set tofron 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 customersGET/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}/suppliersandGET/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 key | Accepted code | Label | Required legal_registration_id format |
|---|---|---|---|
siren | 0002 | SIREN | 9 digits |
siret | 0009 | SIRET | 14 digits |
ridet | 0228 | RIDET | 6 to 10 digits |
tahiti | 0229 | Tahiti | 6 to 9 digits |
european_union | 0223 | European Union | 2 letters then 2 to 30 letters or digits |
international | 0227 | International | 1 to 50 characters, no whitespace |
routing_code | 0224 | Routing code | 14 digits |
b2c | 0226 | B2C | 1 to 30 letters, digits, or hyphens |
aife | 0225 | AIFE (Agence pour l'informatique financière de l'État) | 14 digits |
it_partita_iva | 0211 | Partita IVA (Italy) | IT then 11 digits |
global_location_number | 0088 | Global Location Number | 13 digits |
france_platform | 0238 | French administration platform | 14 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_terms | Label |
|---|---|
seven_days | 7 days |
fifteen_days | 15 days |
thirty_days | 30 days |
thirty_days_end_of_month | 30 days end of month |
forty_five_days | 45 days |
forty_five_days_end_of_month | 45 days end of month |
on_receipt | On receipt |
custom | Custom |
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_format | Format |
|---|---|
ubl | UBL XML |
cii | CII XML |
facturx | Factur-X |
null | No 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_iddesignates the record's accounting ledger. It isnulluntil you set it, and the ledger must belong to the same company as the record (Accounting ledgers).auxiliary_account_numberis the auxiliary account identifying the record in your accounting software. Free-form string,nullby default.offset_accountscarries the record's offset accounts.accounting_setup_completeis read-only and derived: it istrueonly when the record carries anaccounting_ledger_id, anauxiliary_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
idadds an account; - an entry with
idmodifies the matching account and leaves the omitted fields unchanged. Unlikeaddressesandcontacts, no field has to be resent for the change to apply:{ "id": 71, "default": false }is enough; - an entry with
idand_destroy: truedeletes 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
422with the codeduplicate_record, and neither row is written; - creating a new
default: trueaccount in the same call as the demotion of the old one works, in both orders; - deleting the old one with
_destroy: truewhile 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
addressesandcontactsare JSON arrays of objects,payment_methodsa 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-charactercountry_code. Only the length is checked, never the content:12or??are accepted just likeFR. 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_typedefaults tobillingand acceptsbilling,delivery, orboth.default_billingrequiresaddress_typebillingorboth,default_deliveryrequiresdeliveryorboth; a record carries at most onedefault_billingaddress and onedefault_deliveryaddress.default_billingdesignates the address used when the record has more than one. - A contact requires
nameand at least one ofemailorphone. Anemailthat is present must also be a well-formed address, otherwise the record is refused with a422. A record carries at most onedefaultcontact. - An
addressesentry whoseaddress_line_1,city,postal_code, andcountry_codeare all empty, or acontactsentry whosename,email, andphoneare all empty, is discarded before validation: the response is201and the entry is simply absent. A partially filled entry, by contrast, returns422. - Send no
idinaddressesorcontactsat creation: the record has no elements to look up yet, and the call returns404. Reposting the body of aGETresponse 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
idadds an element; - an entry with
idand_destroy: truedeletes the element; - an entry with
idmodifies the element, provided it carries at least one of the fields that decide rejection:address_line_1,city,postal_code, orcountry_codefor an address,name,email, orphonefor a contact. An entry carrying none of them - for example{ "id": 21, "default": true }- is discarded silently: the response is200and the element is unchanged. Resend one of those fields alongside the value you want to change for the update to apply; - an
idthat does not belong to this record returns404, not422.
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
partiesentry carryingparty_idis 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, andbilling_address_iddesignates the address to use when the record has more than one. Two limits worth knowing:party_idis 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 a422, not silently degraded. And an unknownbilling_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'sdefault_billingaddress, 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_numberand offset accounts, and setaccounting_ledger_idwhile it is stillnull; any write to the record itself movesupdated_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 emptyoffset_accountsandaccounting_setup_completeatfalse, and it is returned by theaccounting_setup_status=incompletefilter. Reconciling your own directory against the list, or countingincompleterecords, 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 lastPATCHas 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, andDELETE /api/v1/customers/{id}or/api/v1/suppliers/{id}, the record belongs to a workspace whose IP allowlist refuses the calling address; - the
company_idpassed at creation is unknown in the workspace or, for a customer, lacks the sales capability; company_idis 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, oroffset_accountsentry carries anidthat does not belong to this record, or carries anidwhile 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 withmeta.total_pages; - on lists, a
legal_registration_scheme_idvalue absent from the parameter'senumreturns"Valeur legal_registration_scheme_id invalide. Valeurs autorisées : ..."in the usual error envelope, where the message then lists every accepted key. Branch onerror, which carriesbad_request, not on that enumeration: it grows with every scheme added; - on lists, an
accounting_setup_statusvalue other thancompleteorincompletereturns"Valeur accounting_setup_status invalide. Valeurs autorisées : complete, incomplete"in the same envelope; - on
POSTandPATCH, a request body without the wrappingcustomerorsupplierkey returns a400in the usual{error, message}envelope, witherrorset tobad_requestand 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}/customersreturns200with"data": [];GET,PATCH, andDELETE /api/v1/customers/{id}return404on records that do exist;POST /api/v1/workspaces/{workspace_id}/customersreturns404, whethercompany_iddesignates 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.
Related pages
- Issue a sales invoice - reference a record by
party_idon an invoice - Update invitations - have the party fill in the record itself
- Supporting documents - a record's supporting documents
- Accounting ledgers - the ledger designated by
accounting_ledger_id - API reference: list customers
- API reference: create a customer
- API reference: list suppliers