Evoliz Integration Guidelines for E-commerce

These guidelines describe how to connect an online store to Evoliz: pushing your catalog, your customers and your orders, then invoicing, transmitting and settling them.

They complement the API reference, which is the authoritative description of every endpoint, payload and status code. The reference tells you what each endpoint accepts; this guide tells you in which order to call them, and which traps to avoid. Breaking changes are listed in the changelog.

All examples use plain HTTP so they can be transposed to any language.

1. Before you start

API credentials

In Evoliz, go to Applications, then to the Available Connectors section, and enable the Evoliz API connector (the API tag helps you find it). You can then generate a set of credentials: a public key and a secret key.

The secret key must be stored server-side only. It grants full access to your account data for as long as it is valid.

Your customer number

Every company endpoint is scoped by your customer number (companyid). To find it, log in to Evoliz and look just below your first and last name, in the bottom-left corner: the first part is your customer number.

Base URL and authentication

The base URL is https://www.evoliz.io/.

Authenticate by posting your keys to /api/v1/login:

POST /api/v1/login HTTP/1.1
Host: www.evoliz.io
Content-Type: application/json

{
  "public_key": "YOUR_PUBLIC_KEY",
  "secret_key": "YOUR_SECRET_KEY"
}
{
  "access_token": "eyJ0eXAiOiJKV1Qi...",
  "expires_at": "2026-09-11 14:35:12",
  "scopes": ["company_users", "client", "sale_order", "sale_invoice", "..."]
}

Pass the token on every subsequent call:

Authorization: Bearer YOUR_ACCESS_TOKEN

The token is valid for 20 minutes, as stated by expires_at. Cache it server-side and reuse it until it expires, then log in again — do not authenticate on every call, you will exhaust your rate limit. Never store the token anywhere a browser can read it: it is a server-side secret with the same reach as your keys.

The companies/{companyid} path segment is optional for a company user, so /api/v1/companies/{companyid}/clients and /api/v1/clients are equivalent. The explicit form is used throughout this guide.

Permissions

The scopes returned at login mirror the permissions of the Evoliz user owning the credentials. An e-commerce integration needs:

Area Scope
Customers client
Customer contacts client_contact_client
Catalog article_article
Orders sale_order
Invoices sale_invoice
Payments sale_payment
Credit notes sale_credit

A 403 on an endpoint means the scope is missing: grant the corresponding access to that user in Evoliz, then log in again to obtain a token carrying it. Transforming an order into an invoice requires both sale_order and sale_invoice.

Rate limits

The API allows 100 requests per minute per IP by default. Every response carries:

  • x-ratelimit-limit — the ceiling for the route;
  • x-ratelimit-remaining — how many requests are left in the current minute.

Exceeding it returns 429 Too Many Requests. Since the naive "search then create" loop costs two calls per entity, a single order can easily cost eight or more; a catch-up batch of a few hundred orders will hit the limit. So:

  • read x-ratelimit-remaining and back off before you are throttled, and retry 429 with an exponential delay;
  • cache reference data and the identifiers you have already resolved instead of re-reading them for every order;
  • spread catch-up batches over time rather than firing them in parallel.

There is no webhook: Evoliz never calls your store. Anything you need to observe (a payment recorded in Evoliz, a transmission status) has to be polled, which is a further reason to keep your request budget under control.

2. Reference data

Four lists describe how your account is configured. They change rarely: read them once at startup and cache them, do not fetch them per order.

Endpoint Use
GET /api/v1/companies/{companyid}/vat-rates VAT rates you are allowed to send on a line
GET /api/v1/companies/{companyid}/paytypes Payment methods and their paytypeid
GET /api/v1/companies/{companyid}/payterms Payment terms and their paytermid
GET /api/v1/companies/{companyid}/unit-codes Unit codes accepted on articles

Do not hardcode these values. The identifiers are stable, but which ones your account actually offers is not.

List endpoints are paginated (data, links, meta), 15 results per page by default and up to 100 with per_page.

3. Prices, VAT and discounts

Sending prices inclusive or exclusive of VAT

Most storefronts hold B2C prices inclusive of VAT. Declare that at document level and send your prices as they are:

{
  "prices_include_vat": true,
  "items": [
    { "designation": "Banana mug", "quantity": 2, "unit_price": 14.90, "vat_rate": 20 }
  ]
}

unit_price is interpreted according to prices_include_vat — inclusive of VAT when true, exclusive when false or omitted. It accepts up to 4 decimals and cannot be negative.

The older unit_price_vat_exclude field is deprecated. Use unit_price only; sending both in the same request is rejected.

VAT rates

vat_rate must be one of the rates configured on your account — read them from GET /vat-rates. Any other value returns 400, and the error message lists the accepted rates.

For a line that carries no VAT, omit vat_rate entirely. 0 is not a configurable rate and will be rejected. (A VAT-exempt company carries a document-level exoneration reason instead.)

Shipping costs

Evoliz has no dedicated shipping field. Add shipping as a regular line, with its own VAT rate when it is taxed:

{ "designation": "Frais de livraison", "quantity": 1, "unit_price": 4.90, "vat_rate": 20 }

Discounts

Per-line discount — use rebate on the line. It takes an amount or a percentage string:

{ "designation": "Banana mug", "quantity": 2, "unit_price": 14.90, "vat_rate": 20, "rebate": "10%" }

Order-level discount — use global_rebate on the document, again as an amount or a percentage:

{ "global_rebate": "5%" }

Do not model a discount as a line with a negative unit_price. Negative unit prices are rejected on all sale documents. If your store computes its global discount differently from Evoliz (typically because it applies it on VAT-inclusive totals), send global_rebate as a plain amount rather than a percentage so both systems agree on the figure.

4. Never create a duplicate

Your store is the source of truth; Evoliz must converge towards it. Before creating anything, look for it, and use a key that matches exactly rather than a fuzzy one.

Entity Key to use Filter Match
Article reference ?reference=SKU-42 exact
Client code ?code=CUST-1234 exact
Contact email + clientid ?clientid=1&email=... exact
Order external_document_number ?search=... fuzzy

The best approach is to write your own store identifiers into these fields when you create the record, so that a later lookup is a single exact query: your SKU into the article reference, your customer id into the client code, your order id into the order external_document_number.

Two things to be careful about:

search is not an exact filter. There is no exact filter on external_document_number. The search parameter performs a partial match across several columns at once — document number, external document number, amounts, description and client name. So ?search=1042 can return an order whose amount is 1042, or whose client is named "1042". Always re-check the candidates client-side:

GET /api/v1/companies/{companyid}/sale-orders?search=WEB-1042&period=currentyear

then keep only the result whose external_document_number is exactly WEB-1042.

List endpoints on sale documents apply a default period. GET /sale-orders, GET /invoices and GET /credits are filtered by your account's default period when no period is passed — so an order placed outside that window is invisible, and you would create it a second time. Always pass an explicit period when looking for an existing document:

GET /api/v1/companies/{companyid}/sale-orders?search=WEB-1042&period=custom&date_min=2026-01-01&date_max=2026-12-31

Clients, contacts and articles are not affected: they have no period filter.

5. Catalog

Look the article up by its reference, which is unique per company:

GET /api/v1/companies/{companyid}/articles?reference=SKU-42

If the list is empty, create it:

POST /api/v1/companies/{companyid}/articles
Content-Type: application/json

{
  "reference": "SKU-42",
  "designation": "Banana mug",
  "unit_price": 14.90,
  "vat_rate": 20,
  "ttc": true,
  "nature": "product"
}

reference and designation are required. Note that reference is limited to 25 characters: if your SKUs are longer, derive a stable shortened key rather than truncating blindly, or you will collide two products onto one article. unit_price is optional — you may keep prices in your store only and send them on each order line.

Selling a catalog article is not mandatory. An order line can carry an articleid, or describe a free-form line with designation, quantity and unit_price. Reference the article when you want the sale to feed Evoliz's catalog statistics; use free-form lines for one-offs like shipping.

6. Customers

Looking up and creating

Look the customer up by the code you assigned:

GET /api/v1/companies/{companyid}/clients?code=CUST-1234

If it does not exist, create it:

POST /api/v1/companies/{companyid}/clients
Content-Type: application/json

{
  "code": "CUST-1234",
  "name": "Doe",
  "type": "Particulier",
  "address": {
    "addr": "12 rue des Lilas",
    "postcode": "83130",
    "town": "La Garde",
    "iso2": "FR"
  }
}

name, type and address are required. Inside the address, postcode, town and iso2 are required, and addr — the first address line — is required too.

Note the field names: the address city is town, and the country is iso2 on write. When you read a client back, the country comes as an object: address.country.iso2.

code is optional as far as the API is concerned, but it is what makes your integration idempotent, so always send it. It is limited to 20 characters and must be unique within your company.

Client types

type takes one of three labels:

Value Meaning
Particulier Private individual — the usual B2C case
Professionnel Business
Administration publique Public administration

The type is not cosmetic: it drives which identifiers are mandatory, and how the invoice must later be transmitted. Map it from what your store knows about the buyer — typically, whether a company name or VAT number was supplied at checkout.

Identification requirements

For a private individual, the payload above is complete.

For a French business or public administration, three identifiers are required:

{
  "code": "CUST-1235",
  "name": "Ministère de l'Économie",
  "type": "Professionnel",
  "business_number": "13002526500013",
  "business_identification_number": "130025265",
  "vat_number": "FR00300256549",
  "address": {
    "addr": "139 rue de Bercy",
    "postcode": "75012",
    "town": "Paris",
    "iso2": "FR"
  }
}
Field Content Format
business_number SIRET exactly 14 digits
business_identification_number SIREN exactly 9 digits
vat_number Intra-community VAT number format-checked

Collect these at checkout for your B2B customers. A business order cannot be invoiced without them, and they cannot be guessed afterwards.

For a non-French customer, only vat_number is relevant, and it is optional.

7. Customer contacts

Contacts hold the person to reach for a given client — useful to send the invoice by email. Look one up by client and email:

GET /api/v1/companies/{companyid}/contacts-clients?clientid=1&email=john@example.com

Then create it if needed:

POST /api/v1/companies/{companyid}/contacts-clients
Content-Type: application/json

{
  "clientid": 1,
  "civility": "M.",
  "lastname": "Doe",
  "firstname": "John",
  "email": "john@example.com",
  "favorite": true,
  "label_tel_primary": "6",
  "tel_primary": "0612345678"
}

clientid and lastname are required; email becomes required when favorite is true. label_tel_primary is required whenever you send tel_primary, and takes a numeric label id from the account's list (6 is "Portable").

The API does not enforce uniqueness on a contact's email, so the lookup above is your only protection against creating the same person twice.

8. Creating the order

POST /api/v1/companies/{companyid}/sale-orders
Content-Type: application/json

{
  "external_document_number": "WEB-1042",
  "documentdate": "2026-09-11",
  "clientid": 1,
  "business_process": "goods",
  "prices_include_vat": true,
  "term": {
    "paytermid": 17
  },
  "items": [
    { "articleid": 12, "quantity": 2 },
    { "designation": "Banana mug", "quantity": 1, "unit_price": 14.90, "vat_rate": 20 },
    { "designation": "Frais de livraison", "quantity": 1, "unit_price": 4.90, "vat_rate": 20 }
  ]
}

Required: documentdate, clientid, term.paytermid and a non-empty items array.

external_document_number carries your own order reference. It is unique per company, limited to 40 characters, and is what you match on to avoid creating the order twice.

documentdate uses Y-m-d and cannot be more than 30 days in the future.

term.paytermid is the payment term. For a store that takes payment at checkout, 17 ("A la commande") is the one that reflects reality; 1 ("A réception") means payment on receipt. Read GET /payterms for the full list. Two terms need a companion field: 18 requires term.duedate, and 16 requires term.paydelay and term.endmonth.

Lines either reference a catalog article through articleid — designation, price and VAT are then taken from the article — or describe themselves with designation, quantity and unit_price. An optional type lets you insert presentation lines: text for a comment, sub_total for a subtotal. Those carry no price, and rebate only applies to regular (article) lines.

Business process

business_process states the nature of what you sell:

Value Meaning
goods Goods
services Services
mixed Both

It is the single most important field to get right, because an order without a business process cannot be invoiced: the transformation in the next section will fail, and the field cannot be inferred after the fact. Set it on every order you create.

For a store shipping physical products, that is goods. mixed is rejected when the client is a Particulier, so for a B2C order carrying both goods and services, pick the one that dominates.

After creating the order

The order is created in draft and numbered immediately. Store the returned orderid alongside your own order, together with the clientid and the article ids you resolved: it saves you the lookups on the next run.

Depending on your account settings, an analytic code (analyticid) or a sale classification (items[].sale_classificationid) may be mandatory. If a create call returns 400 naming one of them, that requirement comes from your Evoliz configuration.

9. Invoicing the order

Invoice the order when you have shipped it, or when payment is captured — whichever matches your accounting practice.

POST /api/v1/companies/{companyid}/sale-orders/{orderid}/invoice

This returns a draft invoice, carrying the order's lines, client and business process. Finalize it to get a definitive invoice number:

POST /api/v1/companies/{companyid}/invoices/{invoiceid}/create
Content-Type: application/json

{
  "auto_recovery_enabled": false
}

auto_recovery_enabled is optional; omitted, your account's default applies. Leave automatic dunning off for orders already paid at checkout.

Both steps are one-way. A finalized invoice cannot be edited or deleted — it can only be corrected by a credit note. So check the draft before finalizing it if your order data may still change.

Requires the sale_invoice scope in addition to sale_order.

10. Transmitting the invoice

A finalized invoice destined to a business or a public administration must reach the buyer through a Plateforme Agréée (PA). This is a three-step flow, and it is skipped entirely for private individuals.

Step 1 — find the buyer's electronic address

GET /api/v1/companies/{companyid}/invoices/{invoiceid}/electronic-addresses

The lookup uses the buyer's SIREN as recorded when the invoice was created, so you pass nothing. Two shapes of answer, both 200:

{ "skip": true }

The buyer is out of scope — a private individual, a non-French customer, or a client with no SIREN. Nothing more to do: stop here. This is the normal answer for B2C orders.

{
  "total": 1,
  "results": [
    {
      "addressing_identifier": "0225:130025265_13002526500013_SERVICE-JURIDIQUE",
      "siren": "130025265",
      "siret": "13002526500013",
      "status": "Enabled",
      "business_name": "MINISTERE DE L ECONOMIE ET DES FINANCES",
      "routing_identifier": "SERVICE-JURIDIQUE",
      "needs_legal_commitment": false
    }
  ]
}

Pick a line whose status is Enabled — only those can currently receive an invoice. When several are returned, the choice belongs to the buyer, so record it against the customer rather than asking again on every order.

A 422 with invalid_client_siren means the client's SIREN is malformed: fix the customer record, then invoice again.

Step 2 — set the routing address

PATCH /api/v1/companies/{companyid}/invoices/{invoiceid}/routing-address
Content-Type: application/json

{
  "addressing_identifier": "0225:130025265_13002526500013_SERVICE-JURIDIQUE"
}

addressing_identifier alone identifies the recipient; everything else is re-derived and re-validated server-side. When the chosen address has needs_legal_commitment set, also send legal_commitment_code — the buyer's commitment ("engagement") number, up to 50 characters. Omitting it there returns 422 with legal_commitment_required.

This writes the address onto the invoice's Factur-X. It does not send anything yet.

Step 3 — transmit

POST /api/v1/companies/{companyid}/invoices/{invoiceid}/transmit

Evoliz regenerates the Factur-X, submits it to the PA and returns the PA's response:

{
  "success": true,
  "flow_id": "b1c2d3e4-...",
  "tracking_id": "evoliz-inv-24459-2026-0001",
  "tech_status": "Pending"
}

Persist tracking_id against your order: it is your handle on this invoice for any later reconciliation.

The invoice must be finalized before you transmit it. Transmission is guarded against duplicates, and returns 409 with:

  • already_emitted — the invoice has already been transmitted. Treat it as success; do not retry.
  • retry_too_soon — a previous attempt failed and the retry delay has not elapsed. Retry later.

A 503 means the PA or the dependency is unreachable. The invoice is unchanged, so retry with a backoff.

11. Recording the payment

Record the payment when you actually receive it:

POST /api/v1/companies/{companyid}/invoices/{invoiceid}/payments
Content-Type: application/json

{
  "paydate": "2026-09-11",
  "label": "Stripe ch_3PqR2s...",
  "paytypeid": 3,
  "amount": 34.70
}

paydate, label and paytypeid are required, and so is amount for every payment method but one. label is capped at 80 characters — a good place for your payment-processor reference, which makes reconciliation possible later.

amount cannot exceed what remains to be paid on the invoice; the error message tells you the actual remainder. Partial payments are allowed: post several payments to settle an invoice progressively.

paytypeid identifies the payment method — see the appendix, and read GET /paytypes for what your account offers. One value behaves differently: 13 (Avoir) settles the invoice with an existing credit note. It requires creditid and prohibits amount, which is derived from the credit note. The credit note must belong to the same client and not exceed the remaining balance.

Requires the sale_payment scope.

12. Refunds

A finalized invoice is never modified; it is corrected by a credit note.

There are two ways to create one, and which applies depends on whether the invoice has been paid:

The invoice has no payment recorded — derive the credit note from it:

POST /api/v1/companies/{companyid}/invoices/{invoiceid}/credit

for a full credit, or POST /api/v1/companies/{companyid}/invoices/{invoiceid}/partial-credit with the lines to credit, for a partial one. The client, lines and business process are inherited. On a partial credit, prices_include_vat must match the invoice's own mode.

The invoice has been paid — which is the normal case for a store that settles at checkout — those two endpoints are rejected: a credited invoice must not already carry payments. Create a standalone credit note instead:

POST /api/v1/companies/{companyid}/credits
Content-Type: application/json

{
  "documentdate": "2026-09-20",
  "clientid": 1,
  "business_process": "goods",
  "prices_include_vat": true,
  "invoice_ref": "FA2026-0042",
  "invoice_ref_date": "2026-09-11",
  "term": { "paytermid": 17 },
  "items": [
    { "designation": "Banana mug", "quantity": 1, "unit_price": 14.90, "vat_rate": 20 }
  ]
}

invoice_ref and invoice_ref_date are required, and carry the corrected invoice's number and date — this is what ties the credit note to the original. invoice_ref is limited to 35 characters and accepts only letters, digits, -, + and _. business_process is required as well.

Then finalize it, exactly as for an invoice:

POST /api/v1/companies/{companyid}/credits/{creditid}/create

Credit notes go through the same transmission flow as invoices — electronic-addresses, routing-address, transmit — under /credits/{creditid}/. For a B2C refund, step 1 answers skip: true and you are done.

Requires the sale_credit scope.

13. Handling errors

Code Meaning What to do
400 Validation failed Fix the payload. Never retry as-is.
401 Token missing or expired Log in again, replay the call.
403 Missing scope, or client/document not accessible Grant the access in Evoliz; do not retry blindly.
404 Unknown resource Your stored id is stale; look the resource up again.
409 Already transmitted, or retried too soon See transmitting the invoice.
422 Business rule rejected the request Read the error code; usually the customer data needs fixing.
429 Rate limit exceeded Back off and retry.
503 Dependency unreachable Retry with a backoff; nothing was changed.

Validation errors return 400 — not 422 — with the offending fields:

{
  "error": "Bad Request Error",
  "message": {
    "items.0.vat_rate": ["The items.0.vat_rate must be one of the VAT rates configured for the company: 20, 10, 5.5."]
  }
}

Two habits make a store integration resilient:

Persist the Evoliz identifiers as you obtain them. clientid, articleid, orderid, invoiceid, creditid, tracking_id — stored against your own records, they turn every later call into a direct one and remove most of your rate-limit pressure.

Make each step resumable. Creating the order, invoicing it, finalizing, transmitting and paying are five separate calls, and a run can die between any two of them. Record which step succeeded so a retry picks up where it stopped instead of starting over — starting over is what creates duplicate orders.

Appendix: payment methods

Identifiers are stable, but only those enabled on your account are usable. Read GET /api/v1/companies/{companyid}/paytypes rather than hardcoding this table.

paytypeid Payment method
1 PayPal
2 Virement (wire transfer)
3 Carte bancaire (credit card)
4 Chèque (cheque)
5 Espèces (cash)
6 Autres (other)
7 Chèque Emploi Service Universel (CESU)
8 Prélèvement (direct debit)
9 Lettre de change (bill of exchange)
10 Traite (draft)
11 Chèque vacances (holiday voucher)
12 Contre-remboursement (cash on delivery)
13 Avoir (credit note) — settles with an existing credit note, see recording the payment