# API CaledoCompta (v1)

> Documentation complète de l'API REST de CaledoCompta, logiciel de facturation et de comptabilité (Nouvelle-Calédonie, montants en XPF). Ce fichier est fait pour être donné à un assistant IA qui écrit une intégration : chaque route, chaque champ, ses limites et ses valeurs par défaut.

URL de base : `https://VOTRE-COMPTE.caledocompta.com/api/v1`

## Authentification

Chaque requête s'authentifie par une clé API, dans l'entête `Authorization: Bearer <clé API>`. Les clés se créent dans le logiciel, **Réglages › Documentation API**, et ne sont affichées qu'une fois. Une clé a les droits de l'utilisateur qui l'a créée et reste valable jusqu'à sa révocation : révoquez-la au moindre doute.

```
Authorization: Bearer caledocompta_key_...
Content-Type: application/json
Accept: application/json
```

## Conventions

- **Format** : JSON en entrée et en sortie. Dates `AAAA-MM-JJ`, dates et heures en ISO 8601 avec fuseau.
- **Listes** : Éléments dans `data`, avec `page`, `perPage` et `total`. Factures et paiements, comme le catalogue de la boutique, se parcourent page par page (`page`, `perPage`). Les autres listes renvoient 1 000 éléments au plus.
- **Montants** : En XPF. Devis et factures : prix HT par ligne. Taux dans `tvaBucket` : `tgc22`, `tgc11`, `tgc6`, `tgc3`, `tgc0`. Commandes de la boutique : prix TTC payés.
- **Synchronisation** : Factures et paiements portent `updatedAt` et acceptent `updatedSince` : ne relisez que ce qui a changé depuis votre dernier passage. Un paiement enregistré ou supprimé compte comme une modification de sa facture.
- **Idempotence** : Entête facultatif `Idempotency-Key` sur la création d'une facture, d'un avoir ou d'un paiement. Un appel renvoyé avec la même clé dans les 24 h ne crée rien de plus : il reçoit la réponse d'origine, avec l'entête `Idempotent-Replayed: true`. Même clé pour une autre requête : `422`. Requête d'origine encore en cours : `409`, réessayez. Une clé : 1 à 191 caractères ASCII sans espace, un UUID par exemple.
- **Commandes** : Identifiées par `source` + `reference` : renvoyer une commande ne crée pas de doublon.
- **Limite** : 90 requêtes par minute. Au-delà : `429`.
- **Droits** : Une clé a les droits de l'utilisateur qui l'a créée. Pour restreindre une intégration, créez sa clé depuis un utilisateur dont le rôle est limité.
- **Versions** : L'API `/api/v1` évolue sans rupture : nouveaux champs, nouvelles routes, nouveaux paramètres facultatifs. Ignorez les champs que vous ne connaissez pas. Un changement incompatible passera par une nouvelle version, annoncée au moins 6 mois avant l'arrêt de la précédente.

## Erreurs

Le corps d'une erreur est `{ "code": "...", "message": "...", "fields": { "champ": ["message"] } }`.

| Statut | Signification |
| --- | --- |
| 401 | Clé absente, invalide ou révoquée |
| 402 | Abonnement inactif |
| 403 | Permission insuffisante ou module non activé |
| 404 | Ressource introuvable |
| 409 | Opération impossible dans l'état actuel |
| 422 | Données invalides, détail dans `fields` |
| 429 | Trop de requêtes |

## Routes

### Compte

#### `GET /me` : Vérifier la clé

L'utilisateur de la clé et le compte. Le bon appel pour tester une clé.

Réponse `200` :

```json
{
    "user": {
        "id": 1,
        "firstName": "Teva",
        "lastName": "Exemple",
        "email": "client@example.com",
        "role": "Administrateur"
    },
    "account": {
        "name": "Ma société",
        "tvaEnabled": true,
        "decimals": 0
    }
}
```

### Clients

#### `GET /clients` : Lister les clients

Les clients actifs, par ordre alphabétique (1 000 au plus).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `q` (query) | texte | non |  | Recherche dans le nom du client. |

Réponse `200` :

```json
{
    "data": [
        {
            "id": 42,
            "name": "Acme Sarl",
            "email": "contact@example.com",
            "outstanding": 0,
            "dueCount": 0,
            "invoiceCount": 1,
            "totalBilled": 11600
        }
    ]
}
```

#### `POST /clients` : Créer un client

Crée la fiche client et son contact. Pour réutiliser un client existant, cherchez-le avec `GET /clients?q=`.

Droit requis : clients (écriture).

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `name` | texte | requis | Max 255 caractères | Nom du client ou de la société. |
| `email` | email | non |  | Email du contact, utilisé pour envoyer devis et factures. |
| `phone` | texte | non | Max 40 caractères |  |

Exemple de requête :

```json
{
    "name": "Acme Sarl",
    "email": "contact@example.com",
    "phone": "+687 00 00 00"
}
```

Réponse `201` :

```json
{
    "id": 42,
    "name": "Acme Sarl",
    "email": "contact@example.com",
    "phone": "+687 00 00 00"
}
```

#### `GET /clients/{id}` : Détail d'un client

La fiche, le reste dû, et l'historique des factures, devis et paiements.

Droit requis : clients (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du client. |

Réponse `200` :

```json
{
    "id": 42,
    "name": "Acme Sarl",
    "email": "contact@example.com",
    "phone": "+687 00 00 00",
    "website": null,
    "vat": null,
    "city": "Nouméa",
    "zip": "98800",
    "country": null,
    "outstanding": 0,
    "invoices": [
        {
            "id": 318,
            "number": "FACT-318",
            "date": "2026-09-23",
            "total": 11600,
            "status": "paid"
        }
    ],
    "estimates": [
        {
            "id": 108,
            "number": "DEV-108",
            "date": "2026-09-20",
            "total": 11600,
            "status": "accepted"
        }
    ],
    "payments": [
        {
            "id": 77,
            "date": "2026-09-23",
            "amount": 11600,
            "method": "Virement",
            "invoiceNumber": "FACT-318"
        }
    ]
}
```

- Erreur `404` : Client introuvable.

#### `PUT /clients/{id}` : Modifier un client

Seuls les champs envoyés changent ; un champ envoyé à `null` est effacé.

Droit requis : clients (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du client. |

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `name` | texte | requis | Max 255 caractères |  |
| `email` | email | non |  | Email du contact principal. |
| `phone` | texte | non | Max 60 caractères |  |
| `city` | texte | non | Max 120 caractères |  |
| `zip` | texte | non | Max 30 caractères |  |
| `country` | texte | non | Max 120 caractères |  |

Exemple de requête :

```json
{
    "name": "Acme Sarl"
}
```

Réponse `200` :

```json
{
    "id": 42,
    "name": "Acme Sarl",
    "email": "contact@example.com",
    "phone": "+687 00 00 00",
    "website": null,
    "vat": null,
    "city": "Nouméa",
    "zip": "98800",
    "country": null,
    "outstanding": 0,
    "invoices": [
        {
            "id": 318,
            "number": "FACT-318",
            "date": "2026-09-23",
            "total": 11600,
            "status": "paid"
        }
    ],
    "estimates": [
        {
            "id": 108,
            "number": "DEV-108",
            "date": "2026-09-20",
            "total": 11600,
            "status": "accepted"
        }
    ],
    "payments": [
        {
            "id": 77,
            "date": "2026-09-23",
            "amount": 11600,
            "method": "Virement",
            "invoiceNumber": "FACT-318"
        }
    ]
}
```

- Erreur `404` : Client introuvable.
- Erreur `422` : L'email n'a pas pu être enregistré pour ce client.

### Devis

#### `GET /estimates` : Lister les devis

Les devis, du plus récent au plus ancien (1 000 au plus), sans leurs lignes.

Droit requis : devis (lecture).

Réponse `200` :

```json
{
    "data": [
        {
            "id": 108,
            "number": "DEV-108",
            "clientId": 42,
            "clientName": "Acme Sarl",
            "title": "",
            "status": "sent",
            "subtotal": 10000,
            "discount": {
                "type": null,
                "percentage": 0,
                "amount": 0
            },
            "tvaTotal": 2200,
            "total": 12200,
            "createdAt": "2026-09-28T08:16:30+11:00",
            "date": "2026-09-28",
            "expiryDate": "2026-10-28",
            "lines": []
        }
    ]
}
```

- Statuts : `draft`, `sent`, `accepted`, `declined`, `revised`, `expired`.

#### `POST /estimates` : Créer un devis

Droit requis : devis (écriture).

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `clientId` | entier | requis sans newClient |  | Id du client, renvoyé par `POST /clients`. |
| `newClient` | objet | non |  | Crée le client au passage, si `clientId` est absent. |
| `newClient.name` | texte | requis | Max 255 caractères |  |
| `newClient.email` | email | non |  |  |
| `title` | texte | non | Max 255 caractères | Objet, affiché sous le numéro. |
| `lines` | liste d'objets | non |  | Lignes du devis, dans l'ordre. Facultatif : sans ligne, le devis est créé vide ; sinon, la somme des lignes avant remise doit être supérieure à 0. |
| `lines[].description` | texte | requis | Max 500 caractères | Libellé de la ligne. |
| `lines[].unitPrice` | nombre | non | Min 0, Max 99999999.99, Défaut 0 | Prix unitaire HT. |
| `lines[].quantity` | nombre | requis | Min 0.01, Max 100000 |  |
| `lines[].tvaBucket` | texte | non | Valeurs tgc22 · tgc11 · tgc6 · tgc3 · tgc0, Défaut tgc22 | Taux de TGC de la ligne. |
| `lines[].unit` | texte | non | Max 60 caractères, Défaut u | Unité affichée : h, jour, kg... |
| `lines[].productId` | entier | non |  | Produit du catalogue lié à la ligne (`GET /products`). |
| `lines[].type` | texte | non | Valeurs plain · title · text, Défaut plain | `title` : titre de section, `text` : paragraphe de texte mis en forme. Leur prix, quantité et TGC sont ignorés. |
| `date` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui |  |
| `expiryDate` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui + délai des réglages (30 jours s'il est nul) | Date de validité. |
| `discount` | objet | non |  | Remise sur le total HT. |
| `discount.type` | texte | non | Valeurs percentage · amount |  |
| `discount.value` | nombre | non | Min 0 | Pourcentage (100 au plus) ou montant HT (le total au plus). |
| `terms` | texte | non | Défaut conditions des réglages | Conditions affichées en bas du document, texte mis en forme (gras, italique, listes). `""` les retire. |
| `notes` | texte | non |  | Note interne, jamais affichée au client. |

Exemple de requête :

```json
{
    "clientId": 42,
    "lines": [
        {
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "quantity": 1,
            "tvaBucket": "tgc22"
        }
    ]
}
```

Réponse `201` :

```json
{
    "id": 108,
    "number": "DEV-108",
    "clientId": 42,
    "clientName": "Acme Sarl",
    "title": "",
    "status": "sent",
    "subtotal": 10000,
    "discount": {
        "type": null,
        "percentage": 0,
        "amount": 0
    },
    "tvaTotal": 2200,
    "total": 12200,
    "createdAt": "2026-09-28T08:16:30+11:00",
    "date": "2026-09-28",
    "expiryDate": "2026-10-28",
    "lines": [
        {
            "id": 19,
            "productId": null,
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "unitPriceExact": 10000,
            "quantity": 1,
            "unit": "u",
            "tvaBucket": "tgc22",
            "lineTotal": 10000
        }
    ],
    "tvaBreakdown": [
        {
            "key": "tgc22",
            "label": "TGC 22 %",
            "rate": 22,
            "amount": 2200
        }
    ],
    "terms": "<p>Mauruuru pour votre confiance.</p>",
    "hasSections": false,
    "emails": []
}
```

- Erreur `422` : Client introuvable, somme des lignes nulle, ou montant supérieur à 99 999 999 F.

#### `GET /estimates/{id}` : Détail d'un devis

Le devis avec ses lignes, le détail de la TGC et les emails envoyés.

Droit requis : devis (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du devis. |
| `sections` (query) | booléen | non |  | `1` : inclut aussi les lignes de titre et de texte. |

Réponse `200` :

```json
{
    "id": 108,
    "number": "DEV-108",
    "clientId": 42,
    "clientName": "Acme Sarl",
    "title": "",
    "status": "sent",
    "subtotal": 10000,
    "discount": {
        "type": null,
        "percentage": 0,
        "amount": 0
    },
    "tvaTotal": 2200,
    "total": 12200,
    "createdAt": "2026-09-28T08:16:30+11:00",
    "date": "2026-09-28",
    "expiryDate": "2026-10-28",
    "lines": [
        {
            "id": 19,
            "productId": null,
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "unitPriceExact": 10000,
            "quantity": 1,
            "unit": "u",
            "tvaBucket": "tgc22",
            "lineTotal": 10000
        }
    ],
    "tvaBreakdown": [
        {
            "key": "tgc22",
            "label": "TGC 22 %",
            "rate": 22,
            "amount": 2200
        }
    ],
    "terms": "<p>Mauruuru pour votre confiance.</p>",
    "hasSections": false,
    "emails": []
}
```

- Erreur `404` : Devis introuvable.

#### `GET /estimates/{id}/pdf` : Télécharger le PDF

Le devis en PDF (`application/pdf`), tel que l'imprime le logiciel.

Droit requis : devis (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du devis. |

Réponse `200` : fichier `application/pdf`.

- Erreur `404` : Devis introuvable.

#### `GET /estimates/{id}/share` : Obtenir le lien public

Un lien à envoyer au client pour voir et télécharger le devis, sans compte.

Droit requis : devis (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du devis. |

Réponse `200` :

```json
{
    "shareUrl": "https://VOTRE-COMPTE.caledocompta.com/…"
}
```

- Erreur `404` : Devis introuvable.

#### `POST /estimates/{id}/send` : Envoyer par email

Envoie le devis en PDF à l'email du client. Le statut du devis ne change pas.

Droit requis : devis (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du devis. |

Réponse `200` :

```json
{
    "ok": true,
    "status": "sent"
}
```

- Erreur `404` : Devis introuvable.
- Erreur `422` : Le client n'a pas d'email (`no_recipient`).

#### `POST /estimates/{id}/convert` : Convertir en facture

Crée la facture du devis, datée du jour ; le devis est conservé. Si le devis a déjà été converti, renvoie la même facture (`200`).

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du devis. |

Réponse `201` :

```json
{
    "id": 319,
    "number": "FACT-319"
}
```

Réponse `200` : Devis déjà converti : la facture existante.

```json
{
    "id": 319,
    "number": "FACT-319"
}
```

- Erreur `404` : Devis introuvable.

### Factures

#### `GET /invoices` : Lister les factures

Les factures, de la plus récente à la plus ancienne, avec le payé (`paid`) et le reste dû (`balance`), sans leurs lignes.

Droit requis : factures (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `status` (query) | texte | non |  | Un ou plusieurs statuts, séparés par des virgules : `draft`, `due`, `overdue`, `part_paid`, `paid`, `credit`. |
| `clientId` (query) | entier | non |  | Les factures de ce client. |
| `dateFrom` (query) | date | non | Format AAAA-MM-JJ | Date de facture, à partir de ce jour inclus. |
| `dateTo` (query) | date | non | Format AAAA-MM-JJ | Date de facture, jusqu'à ce jour inclus. |
| `updatedSince` (query) | date et heure | non | Format ISO 8601 | Les factures modifiées ou payées depuis cet instant. Le fuseau de la date est respecté. |
| `page` (query) | entier | non | Min 1, Défaut 1 | Numéro de page. |
| `perPage` (query) | entier | non | Min 1, Max 1000, Défaut 1000 | Éléments par page. |

Réponse `200` :

```json
{
    "data": [
        {
            "id": 318,
            "number": "FACT-318",
            "clientId": 42,
            "clientName": "Acme Sarl",
            "title": "",
            "status": "due",
            "subtotal": 10000,
            "discount": {
                "type": null,
                "percentage": 0,
                "amount": 0
            },
            "tvaTotal": 2200,
            "total": 12200,
            "createdAt": "2026-09-28T08:16:30+11:00",
            "date": "2026-09-28",
            "updatedAt": "2026-09-28T08:16:30+11:00",
            "dueDate": "2026-10-28",
            "lines": [],
            "paid": 0,
            "balance": 12200
        }
    ],
    "page": 1,
    "perPage": 100,
    "total": 1
}
```

- Statuts : `draft`, `due`, `overdue`, `part_paid`, `paid`, `credit` (avoir).
- Pour suivre les règlements : appelez régulièrement `GET /invoices?updatedSince=` avec l'instant de votre appel précédent.

#### `POST /invoices` : Créer une facture

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` (entête) | texte | non | Max 191 caractères | Entête. Rejouer l'appel avec la même clé ne crée rien de plus. Voir **Conventions › Idempotence**. |

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `clientId` | entier | requis sans newClient |  | Id du client, renvoyé par `POST /clients`. |
| `newClient` | objet | non |  | Crée le client au passage, si `clientId` est absent. |
| `newClient.name` | texte | requis | Max 255 caractères |  |
| `newClient.email` | email | non |  |  |
| `title` | texte | non | Max 255 caractères | Objet, affiché sous le numéro. |
| `lines` | liste d'objets | non |  | Lignes de la facture, dans l'ordre. Facultatif : sans ligne, la facture est créée vide ; sinon, la somme des lignes avant remise doit être supérieure à 0. |
| `lines[].description` | texte | requis | Max 500 caractères | Libellé de la ligne. |
| `lines[].unitPrice` | nombre | non | Min 0, Max 99999999.99, Défaut 0 | Prix unitaire HT. |
| `lines[].quantity` | nombre | requis | Min 0.01, Max 100000 |  |
| `lines[].tvaBucket` | texte | non | Valeurs tgc22 · tgc11 · tgc6 · tgc3 · tgc0, Défaut tgc22 | Taux de TGC de la ligne. |
| `lines[].unit` | texte | non | Max 60 caractères, Défaut u | Unité affichée : h, jour, kg... |
| `lines[].productId` | entier | non |  | Produit du catalogue lié à la ligne (`GET /products`). |
| `lines[].type` | texte | non | Valeurs plain · title · text, Défaut plain | `title` : titre de section, `text` : paragraphe de texte mis en forme. Leur prix, quantité et TGC sont ignorés. |
| `date` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui |  |
| `dueDate` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui + délai des réglages (30 jours s'il est nul) | Date d'échéance. |
| `discount` | objet | non |  | Remise sur le total HT. |
| `discount.type` | texte | non | Valeurs percentage · amount |  |
| `discount.value` | nombre | non | Min 0 | Pourcentage (100 au plus) ou montant HT (le total au plus). |
| `terms` | texte | non | Défaut conditions des réglages | Conditions affichées en bas du document, texte mis en forme (gras, italique, listes). `""` les retire. |
| `notes` | texte | non |  | Note interne, jamais affichée au client. |

Exemple de requête :

```json
{
    "clientId": 42,
    "lines": [
        {
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "quantity": 1,
            "tvaBucket": "tgc22"
        }
    ]
}
```

Réponse `201` :

```json
{
    "id": 318,
    "number": "FACT-318",
    "clientId": 42,
    "clientName": "Acme Sarl",
    "title": "",
    "status": "due",
    "subtotal": 10000,
    "discount": {
        "type": null,
        "percentage": 0,
        "amount": 0
    },
    "tvaTotal": 2200,
    "total": 12200,
    "createdAt": "2026-09-28T08:16:30+11:00",
    "date": "2026-09-28",
    "updatedAt": "2026-09-28T08:16:30+11:00",
    "dueDate": "2026-10-28",
    "lines": [
        {
            "id": 19,
            "productId": null,
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "unitPriceExact": 10000,
            "quantity": 1,
            "unit": "u",
            "tvaBucket": "tgc22",
            "lineTotal": 10000
        }
    ],
    "tvaBreakdown": [
        {
            "key": "tgc22",
            "label": "TGC 22 %",
            "rate": 22,
            "amount": 2200
        }
    ],
    "terms": "<p>Mauruuru pour votre confiance.</p>",
    "hasSections": false,
    "payments": [],
    "paid": 0,
    "balance": 12200,
    "emails": []
}
```

- Erreur `422` : Client introuvable, somme des lignes nulle, ou montant supérieur à 99 999 999 F.

#### `GET /invoices/{id}` : Détail d'une facture

La facture avec ses lignes, le détail de la TGC, les paiements et les emails envoyés.

Droit requis : factures (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant de la facture. |
| `sections` (query) | booléen | non |  | `1` : inclut aussi les lignes de titre et de texte. |

Réponse `200` :

```json
{
    "id": 318,
    "number": "FACT-318",
    "clientId": 42,
    "clientName": "Acme Sarl",
    "title": "",
    "status": "due",
    "subtotal": 10000,
    "discount": {
        "type": null,
        "percentage": 0,
        "amount": 0
    },
    "tvaTotal": 2200,
    "total": 12200,
    "createdAt": "2026-09-28T08:16:30+11:00",
    "date": "2026-09-28",
    "updatedAt": "2026-09-28T08:16:30+11:00",
    "dueDate": "2026-10-28",
    "lines": [
        {
            "id": 19,
            "productId": null,
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "unitPriceExact": 10000,
            "quantity": 1,
            "unit": "u",
            "tvaBucket": "tgc22",
            "lineTotal": 10000
        }
    ],
    "tvaBreakdown": [
        {
            "key": "tgc22",
            "label": "TGC 22 %",
            "rate": 22,
            "amount": 2200
        }
    ],
    "terms": "<p>Mauruuru pour votre confiance.</p>",
    "hasSections": false,
    "payments": [],
    "paid": 0,
    "balance": 12200,
    "emails": []
}
```

- Erreur `404` : Facture introuvable.

#### `GET /invoices/{id}/pdf` : Télécharger le PDF

La facture en PDF (`application/pdf`).

Droit requis : factures (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant de la facture. |

Réponse `200` : fichier `application/pdf`.

- Erreur `404` : Facture introuvable.

#### `GET /invoices/{id}/share` : Obtenir le lien public

Un lien à envoyer au client pour voir et télécharger la facture, sans compte.

Droit requis : factures (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant de la facture. |

Réponse `200` :

```json
{
    "shareUrl": "https://VOTRE-COMPTE.caledocompta.com/…"
}
```

- Erreur `404` : Facture introuvable.

#### `POST /invoices/{id}/send` : Envoyer par email

Envoie la facture en PDF à l'email du client.

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant de la facture. |

Réponse `200` :

```json
{
    "ok": true,
    "status": "due"
}
```

- Erreur `404` : Facture introuvable.
- Erreur `422` : Le client n'a pas d'email (`no_recipient`).

#### `POST /invoices/{id}/credit` : Annuler par un avoir

Pour annuler une facture émise, créez son avoir : il porte ses propres numéro et date. Renvoie l'avoir.

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant de la facture. |
| `Idempotency-Key` (entête) | texte | non | Max 191 caractères | Entête. Rejouer l'appel avec la même clé ne crée rien de plus. Voir **Conventions › Idempotence**. |

Réponse `201` :

```json
{
    "id": 319,
    "number": "FACT-319",
    "clientId": 42,
    "clientName": "Acme Sarl",
    "title": "",
    "status": "credit",
    "subtotal": -10000,
    "discount": {
        "type": null,
        "percentage": 0,
        "amount": 0
    },
    "tvaTotal": -2200,
    "total": -12200,
    "createdAt": "2026-09-30T09:12:04+11:00",
    "date": "2026-09-30",
    "updatedAt": "2026-09-30T09:12:04+11:00",
    "dueDate": "2026-10-28",
    "lines": [
        {
            "id": 20,
            "productId": null,
            "description": "Prestation de conseil",
            "unitPrice": 10000,
            "unitPriceExact": 10000,
            "quantity": -1,
            "unit": "u",
            "tvaBucket": "tgc22",
            "lineTotal": -10000
        }
    ],
    "tvaBreakdown": [
        {
            "key": "tgc22",
            "label": "TGC 22 %",
            "rate": 22,
            "amount": -2200
        }
    ],
    "terms": "<p>Mauruuru pour votre confiance.</p>",
    "hasSections": false,
    "payments": [],
    "paid": 0,
    "balance": 0,
    "emails": []
}
```

- L'avoir apparaît dans `GET /invoices` avec le statut `credit` et des montants négatifs.
- Erreur `404` : Facture introuvable.
- Erreur `422` : La facture est déjà un avoir.

### Paiements & dépenses

#### `GET /payments` : Lister les paiements

Les paiements reçus, du plus récent au plus ancien.

Droit requis : factures (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `invoiceId` (query) | entier | non |  | Les paiements de cette facture. |
| `clientId` (query) | entier | non |  | Les paiements de ce client. |
| `dateFrom` (query) | date | non | Format AAAA-MM-JJ | Date du paiement, à partir de ce jour inclus. |
| `dateTo` (query) | date | non | Format AAAA-MM-JJ | Date du paiement, jusqu'à ce jour inclus. |
| `updatedSince` (query) | date et heure | non | Format ISO 8601 | Les paiements enregistrés ou modifiés depuis cet instant. |
| `page` (query) | entier | non | Min 1, Défaut 1 | Numéro de page. |
| `perPage` (query) | entier | non | Min 1, Max 1000, Défaut 100 | Éléments par page. |

Réponse `200` :

```json
{
    "data": [
        {
            "id": 77,
            "invoiceId": 318,
            "invoiceNumber": "FACT-318",
            "clientId": 42,
            "date": "2026-09-30",
            "amount": 11600,
            "method": "Virement",
            "notes": null,
            "createdAt": "2026-09-30T09:05:41+11:00",
            "updatedAt": "2026-09-30T09:05:41+11:00"
        }
    ],
    "page": 1,
    "perPage": 100,
    "total": 1
}
```

- Un paiement supprimé disparaît de cette liste ; sa facture remonte dans `GET /invoices?updatedSince=` avec son nouveau reste dû.

#### `POST /payments` : Enregistrer un paiement

Un paiement reçu sur une facture ; son statut est recalculé (payée, partiellement payée).

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` (entête) | texte | non | Max 191 caractères | Entête. Rejouer l'appel avec la même clé ne crée rien de plus. Voir **Conventions › Idempotence**. |

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `invoiceId` | entier | requis |  |  |
| `amount` | nombre | requis | Min 1, Max 99999999 | Montant reçu, arrondi au franc. |
| `date` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui |  |
| `method` | texte | non | Max 60 caractères, Défaut Espèces |  |
| `notes` | texte | non | Max 1000 caractères |  |

Exemple de requête :

```json
{
    "invoiceId": 318,
    "amount": 11600,
    "method": "Virement"
}
```

Réponse `201` :

```json
{
    "ok": true,
    "invoiceId": 318,
    "status": "paid"
}
```

- `status` : le statut de la facture après le paiement (`paid`, `part_paid`...).
- Erreur `404` : Facture introuvable.

#### `GET /expenses` : Lister les dépenses

Les dépenses créées par l'utilisateur de la clé, de la plus récente à la plus ancienne (1 000 au plus).

Droit requis : dépenses (lecture).

Réponse `200` :

```json
{
    "data": [
        {
            "id": 55,
            "amount": 12200,
            "tva": {
                "tgc22": 2200
            },
            "title": "Fournitures de bureau",
            "supplierId": null,
            "supplierName": null,
            "categoryId": 3,
            "clientId": null,
            "date": "2026-09-20",
            "paymentStatus": "paid",
            "paymentMethod": "Carte bancaire",
            "paymentDate": "2026-09-20",
            "dueDate": null,
            "notes": null,
            "attachmentId": null,
            "attachmentUrl": null,
            "attachmentType": null,
            "attachmentName": null,
            "createdAt": "2026-09-20T10:02:11+11:00"
        }
    ]
}
```

#### `POST /expenses` : Créer une dépense

Droit requis : dépenses (écriture).

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `amount` | nombre | requis | Min -99999999, Max 99999999 | Montant TTC payé. Négatif pour un avoir fournisseur. |
| `date` | date | requis | Format AAAA-MM-JJ |  |
| `title` | texte | non | Max 255 caractères | Libellé de la dépense. |
| `tva` | objet | non |  | TGC récupérable, par taux. |
| `tva.tgc22` | entier | non | Défaut 0 | Montant de TGC à 22 %, en francs. |
| `tva.tgc11` | entier | non | Défaut 0 | Montant de TGC à 11 %, en francs. |
| `tva.tgc6` | entier | non | Défaut 0 | Montant de TGC à 6 %, en francs. |
| `tva.tgc3` | entier | non | Défaut 0 | Montant de TGC à 3 %, en francs. |
| `categoryId` | entier | non | Défaut catégorie par défaut | Catégorie de dépense. |
| `supplierId` | entier | non |  | Fournisseur, si le module Fournisseurs est actif. |
| `clientId` | entier | non |  | Client concerné. |
| `paymentStatus` | texte | non | Valeurs paid · pending, Défaut paid | `pending` : dépense à payer, avec `dueDate`. |
| `paymentMethod` | texte | non | Max 255 caractères |  |
| `paymentDate` | date | non | Format AAAA-MM-JJ, Défaut date de la dépense | Si payée. |
| `dueDate` | date | non | Format AAAA-MM-JJ | Échéance, si à payer. |
| `notes` | texte | non |  |  |

Exemple de requête :

```json
{
    "amount": 11600,
    "date": "2026-09-20",
    "title": "Fournitures de bureau",
    "tva": {
        "tgc22": 2200
    },
    "paymentMethod": "Carte bancaire"
}
```

Réponse `201` :

```json
{
    "id": 55,
    "amount": 12200,
    "tva": {
        "tgc22": 2200
    },
    "title": "Fournitures de bureau",
    "supplierId": null,
    "supplierName": null,
    "categoryId": 3,
    "clientId": null,
    "date": "2026-09-20",
    "paymentStatus": "paid",
    "paymentMethod": "Carte bancaire",
    "paymentDate": "2026-09-20",
    "dueDate": null,
    "notes": null,
    "attachmentId": null,
    "attachmentUrl": null,
    "attachmentType": null,
    "attachmentName": null,
    "createdAt": "2026-09-20T10:02:11+11:00"
}
```

### Produits

#### `GET /products` : Lister les produits

Le catalogue, par ordre alphabétique (200 au plus) : de quoi remplir des lignes de devis ou de facture.

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `q` (query) | texte | non |  | Recherche dans la désignation. |

Réponse `200` :

```json
{
    "data": [
        {
            "id": 12,
            "name": "Miel de Nouvelle-Calédonie 500 g",
            "unitPrice": 1500,
            "unitPriceExact": 1500,
            "tvaBucket": "tgc3",
            "unit": "pot",
            "kind": "produit"
        }
    ]
}
```

#### `POST /products` : Créer un produit

Droit requis : produits (écriture).

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `name` | texte | requis | Max 255 caractères | Désignation. |
| `unitPrice` | nombre | requis | Min 0, Max 99999999.99 | Prix unitaire HT. |
| `tvaBucket` | texte | non | Valeurs tgc22 · tgc11 · tgc6 · tgc3 · tgc0, Défaut tgc11 | Taux de TGC du produit. |
| `unit` | texte | non | Max 50 caractères, Défaut / |  |
| `kind` | texte | non | Valeurs produit · service, Défaut produit | Nature du produit : livraison de biens ou prestation de services. |

Exemple de requête :

```json
{
    "name": "Miel de Nouvelle-Calédonie 500 g",
    "unitPrice": 1500,
    "tvaBucket": "tgc3",
    "unit": "pot"
}
```

Réponse `201` :

```json
{
    "id": 12,
    "name": "Miel de Nouvelle-Calédonie 500 g",
    "unitPrice": 1500,
    "unitPriceExact": 1500,
    "tvaBucket": "tgc3",
    "unit": "pot",
    "kind": "produit"
}
```

- La référence (SKU), le stock, les descriptions et les images se gèrent dans le logiciel.

#### `PUT /products/{id}` : Modifier un produit

Seuls les champs envoyés changent. Cette route ne modifie pas le stock.

Droit requis : produits (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du produit. |

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `name` | texte | non | Max 255 caractères |  |
| `unitPrice` | nombre | non | Min 0, Max 99999999.99 | Prix unitaire HT. |
| `tvaBucket` | texte | non | Valeurs tgc22 · tgc11 · tgc6 · tgc3 · tgc0 | Taux de TGC du produit. |
| `unit` | texte | non | Max 50 caractères |  |
| `kind` | texte | non | Valeurs produit · service |  |

Exemple de requête :

```json
{
    "unitPrice": 1600
}
```

Réponse `200` :

```json
{
    "id": 12,
    "name": "Miel de Nouvelle-Calédonie 500 g",
    "unitPrice": 1500,
    "unitPriceExact": 1500,
    "tvaBucket": "tgc3",
    "unit": "pot",
    "kind": "produit"
}
```

- Erreur `404` : Produit introuvable.
- Erreur `422` : Aucun champ à modifier.

#### `DELETE /products/{id}` : Supprimer un produit

Supprime le produit et ses images. Les factures déjà émises gardent leurs lignes.

Droit requis : produits (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `id` (chemin) | entier | requis |  | Identifiant du produit. |

Réponse `200` :

```json
{
    "ok": true
}
```

- Erreur `404` : Produit introuvable.

### Boutique en ligne

#### `GET /shop/products` : Lire le catalogue et le stock

Les produits du catalogue avec leur prix, leur stock, leur catégorie, leurs descriptions et leurs images. Une synchronisation régulière ne demande que les produits modifiés depuis la précédente.

Droit requis : produits (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `updatedSince` (query) | date et heure | non | Format ISO 8601 | Seulement les produits modifiés depuis cette date (stock et images compris). |
| `sku` (query) | texte | non | Max 100 caractères | Un seul produit, par sa référence. |
| `hasSku` (query) | entier | non | Valeurs 0 · 1 | `1` : seulement les produits qui ont une référence, c'est-à-dire ceux vendus en ligne. |
| `page` (query) | entier | non | Min 1, Défaut 1 | Numéro de page. |
| `perPage` (query) | entier | non | Min 1, Max 200, Défaut 100 | Éléments par page. |

Réponse `200` :

```json
{
    "data": [
        {
            "id": 12,
            "sku": "MIEL-500",
            "name": "Miel de Nouvelle-Calédonie 500 g",
            "unit": "pot",
            "category": {
                "id": 14,
                "name": "Épicerie fine"
            },
            "shortDescription": "Miel de fleurs récolté à La Foa.",
            "description": "<p>Miel cru, non chauffé, mis en pot à la main.</p>",
            "images": [
                {
                    "id": 301,
                    "url": "https://VOTRE-COMPTE.caledocompta.com/…/miel-500.jpg",
                    "thumbnailUrl": "https://VOTRE-COMPTE.caledocompta.com/…/miel-500-thumb.jpg",
                    "name": "miel-500.jpg",
                    "position": 0
                }
            ],
            "priceHt": 1500,
            "tvaRate": 3,
            "priceTtc": 1545,
            "stockManaged": true,
            "stock": 42,
            "lowStockAlert": 5,
            "updatedAt": "2026-09-23T09:14:02+11:00"
        }
    ],
    "page": 1,
    "perPage": 100,
    "total": 1
}
```

- `stock` et `lowStockAlert` valent `null` si le stock n'est pas suivi. Le stock est en unités entières. `priceTtc` est arrondi à la précision du compte (franc, ou centime pour un compte à 2 décimales).
- Correspondance WooCommerce : `priceTtc` → `regular_price`, `shortDescription` → `short_description`, `category.name` → `categories`, `images[].url` → `images[].src`, `stock` → `stock_quantity`. Les URL d'images sont publiques.
- Comparez régulièrement avec le catalogue complet pour détecter les produits retirés.
- Erreur `403` : Module boutique non activé sur ce compte (`module_disabled`).

#### `POST /shop/orders` : Facturer une commande de la boutique

Retrouve ou crée le client (par email), crée la facture au prix payé et, si la commande est payée, le paiement. Le stock est décrémenté.

Droit requis : factures (écriture).

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `reference` | texte | requis | Max 100 caractères, Caractères A-Z a-z 0-9 . _ - | Numéro de la commande dans la boutique. Renvoyer une commande déjà reçue ne crée rien de plus. |
| `source` | texte | non | Max 50 caractères, Défaut woocommerce | Boutique d'origine. Avec `reference`, identifie la commande. |
| `date` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui | Date de la facture. Un horodatage UTC est ramené au jour de Nouméa. Échéance : le même jour si la commande est payée, sinon cette date + le délai de paiement des réglages (30 jours s'il est nul). |
| `customer` | objet | requis |  | L'acheteur. Un client existant avec le même email est réutilisé, sinon il est créé. |
| `customer.email` | email | requis | Max 255 caractères |  |
| `customer.company` | texte | non | Max 255 caractères | Nom du client. À défaut : prénom et nom, puis l'email. |
| `customer.firstName` | texte | non | Max 100 caractères | Prénom du contact. |
| `customer.lastName` | texte | non | Max 100 caractères | Nom du contact. |
| `customer.phone` | texte | non | Max 40 caractères |  |
| `customer.address` | objet | non |  | Adresse de facturation. |
| `customer.address.street` | texte | non | Max 255 caractères |  |
| `customer.address.city` | texte | non | Max 100 caractères |  |
| `customer.address.zip` | texte | non | Max 20 caractères |  |
| `customer.address.country` | texte | non | Max 100 caractères |  |
| `lines` | liste d'objets | requis | Min 1 élément | Les articles commandés, dans l'ordre de la facture. |
| `lines[].sku` | texte | requis | Max 100 caractères | Référence du produit dans CaledoCompta, majuscules indifférentes. Une référence inconnue refuse toute la commande. |
| `lines[].quantity` | nombre | requis | Min 0.01, Max 100000 | Nombre entier si le stock du produit est suivi. |
| `lines[].unitPriceTtc` | nombre | requis | Min 0, Max 99999999 | Prix unitaire TTC réellement payé, coupons déduits. Le HT est recalculé avec le taux de TGC du produit. |
| `lines[].name` | texte | non | Max 500 caractères, Défaut la désignation du produit | Libellé de la ligne sur la facture. |
| `shipping` | objet | non |  | Frais de livraison, ajoutés en dernière ligne de la facture. |
| `shipping.amountTtc` | nombre | requis avec shipping | Min 0, Max 99999999 | Montant TTC payé. |
| `shipping.tvaRate` | entier | requis si montant > 0 | Valeurs 0 · 3 · 6 · 11 · 22 | Taux de TGC des frais, en %. |
| `shipping.label` | texte | non | Max 255 caractères, Défaut Frais de livraison |  |
| `payment` | objet | requis |  |  |
| `payment.status` | texte | requis | Valeurs paid · pending | `paid` : la facture est soldée. `pending` : elle reste due, à payer plus tard avec `POST /shop/orders/{reference}/payment`. |
| `payment.method` | texte | non | Max 60 caractères, Défaut Boutique en ligne | Moyen de paiement affiché sur le paiement. |
| `payment.transactionId` | texte | non | Max 100 caractères | Identifiant du paiement chez le prestataire, affiché sur le paiement. |
| `payment.date` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui |  |
| `totalTtc` | nombre | non | Min 0 | Total payé sur la boutique. Il est comparé au total de la facture dans `totals`. |
| `notes` | texte | non | Max 2000 caractères | Note ajoutée à la facture. |

Exemple de requête :

```json
{
    "reference": "1042",
    "customer": {
        "email": "client@example.com",
        "firstName": "Teva",
        "lastName": "Exemple",
        "phone": "+687 00 00 00",
        "address": {
            "street": "Rue du Commerce",
            "city": "Nouméa",
            "zip": "98800"
        }
    },
    "lines": [
        {
            "sku": "MIEL-500",
            "quantity": 2,
            "unitPriceTtc": 1545
        }
    ],
    "shipping": {
        "amountTtc": 1000,
        "tvaRate": 11,
        "label": "Livraison Nouméa"
    },
    "payment": {
        "status": "paid",
        "method": "Carte bancaire"
    },
    "totalTtc": 4090
}
```

Réponse `201` :

```json
{
    "source": "woocommerce",
    "reference": "1042",
    "status": "invoiced",
    "clientId": 57,
    "invoice": {
        "id": 318,
        "number": "FACT-318",
        "clientId": 57,
        "clientName": "Teva Exemple",
        "title": "Commande en ligne n° 1042",
        "status": "paid",
        "subtotal": 3901,
        "discount": {
            "type": null,
            "percentage": 0,
            "amount": 0
        },
        "tvaTotal": 189,
        "total": 4090,
        "createdAt": "2026-09-23T09:20:11+11:00",
        "date": "2026-09-23",
        "dueDate": "2026-09-23",
        "lines": []
    },
    "creditNote": null,
    "totals": {
        "invoice": 4090,
        "site": 4090,
        "difference": 0
    },
    "stock": [
        {
            "sku": "MIEL-500",
            "stock": 40
        }
    ],
    "paymentRecorded": true
}
```

Réponse `200` : Commande déjà reçue : rien n'est recréé.

```json
{
    "source": "woocommerce",
    "reference": "1042",
    "status": "invoiced",
    "clientId": 57,
    "invoice": {
        "id": 318,
        "number": "FACT-318",
        "clientId": 57,
        "clientName": "Teva Exemple",
        "title": "Commande en ligne n° 1042",
        "status": "paid",
        "subtotal": 3901,
        "discount": {
            "type": null,
            "percentage": 0,
            "amount": 0
        },
        "tvaTotal": 189,
        "total": 4090,
        "createdAt": "2026-09-23T09:20:11+11:00",
        "date": "2026-09-23",
        "dueDate": "2026-09-23",
        "lines": []
    },
    "creditNote": null,
    "totals": {
        "invoice": 4090,
        "site": 4090,
        "difference": 0
    },
    "stock": [
        {
            "sku": "MIEL-500",
            "stock": 40
        }
    ],
    "duplicate": true
}
```

- **Stock** : chaque réponse (réception, paiement, annulation) renvoie `stock`, le stock à jour des produits de la commande dont le stock est suivi. Reportez-le dans la boutique, qui décrémente aussi son propre stock.
- Une commande déjà reçue renvoie `200` et `"duplicate": true` : un envoi peut être rejoué sans risque.
- `paymentRecorded` indique si le paiement a été enregistré. S'il vaut `false`, enregistrez-le avec `POST /shop/orders/{reference}/payment`.
- Erreur `403` : Module boutique non activé sur ce compte (`module_disabled`).
- Erreur `422` : Référence de produit inconnue, quantité non entière pour un produit dont le stock est suivi, total nul ou taux de TGC des frais manquant : rien n'est créé.
- Erreur `409` : Opération impossible dans l'état actuel de la commande : réessayez plus tard ou consultez `GET /shop/orders/{reference}`.

#### `GET /shop/orders/{reference}` : État d'une commande facturée

La commande, sa facture et, si elle a été annulée, son avoir.

Droit requis : factures (lecture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `reference` (chemin) | texte | requis | Max 100 caractères, Caractères A-Z a-z 0-9 . _ - | Numéro de la commande dans la boutique. |
| `source` (query) | texte | non | Max 50 caractères, Défaut woocommerce | Boutique d'origine, si la commande a été envoyée avec une autre `source`. |

Réponse `200` :

```json
{
    "source": "woocommerce",
    "reference": "1042",
    "status": "invoiced",
    "clientId": 57,
    "invoice": {
        "id": 318,
        "number": "FACT-318",
        "clientId": 57,
        "clientName": "Teva Exemple",
        "title": "Commande en ligne n° 1042",
        "status": "paid",
        "subtotal": 3901,
        "discount": {
            "type": null,
            "percentage": 0,
            "amount": 0
        },
        "tvaTotal": 189,
        "total": 4090,
        "createdAt": "2026-09-23T09:20:11+11:00",
        "date": "2026-09-23",
        "dueDate": "2026-09-23",
        "lines": []
    },
    "creditNote": null,
    "totals": {
        "invoice": 4090,
        "site": 4090,
        "difference": 0
    }
}
```

- Erreur `404` : Aucune commande avec cette référence (et cette source).
- Erreur `403` : Module boutique non activé sur ce compte (`module_disabled`).

#### `POST /shop/orders/{reference}/cancel` : Annuler une commande

Annulation totale : crée un avoir et remet le stock. Le remboursement se fait dans votre moyen de paiement. Une commande déjà annulée renvoie la même réponse, sans second avoir.

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `reference` (chemin) | texte | requis | Max 100 caractères, Caractères A-Z a-z 0-9 . _ - | Numéro de la commande dans la boutique. |
| `source` (query) | texte | non | Max 50 caractères, Défaut woocommerce | Boutique d'origine, si la commande a été envoyée avec une autre `source`. |

Réponse `200` :

```json
{
    "source": "woocommerce",
    "reference": "1042",
    "status": "cancelled",
    "clientId": 57,
    "invoice": {
        "id": 318,
        "number": "FACT-318",
        "clientId": 57,
        "clientName": "Teva Exemple",
        "title": "Commande en ligne n° 1042",
        "status": "paid",
        "subtotal": 3901,
        "discount": {
            "type": null,
            "percentage": 0,
            "amount": 0
        },
        "tvaTotal": 189,
        "total": 4090,
        "createdAt": "2026-09-23T09:20:11+11:00",
        "date": "2026-09-23",
        "dueDate": "2026-09-23",
        "lines": []
    },
    "creditNote": {
        "id": 325,
        "number": "FACT-325",
        "clientId": 57,
        "clientName": "Teva Exemple",
        "title": "Commande en ligne n° 1042",
        "status": "credit",
        "subtotal": -3901,
        "discount": {
            "type": null,
            "percentage": 0,
            "amount": 0
        },
        "tvaTotal": -189,
        "total": -4090,
        "createdAt": "2026-09-24T08:02:40+11:00",
        "date": "2026-09-24",
        "dueDate": "2026-09-24",
        "lines": []
    },
    "totals": {
        "invoice": 4090,
        "site": 4090,
        "difference": 0
    },
    "stock": [
        {
            "sku": "MIEL-500",
            "stock": 42
        }
    ]
}
```

- Erreur `404` : Aucune commande avec cette référence (et cette source).
- Erreur `403` : Module boutique non activé sur ce compte (`module_disabled`).
- Erreur `409` : Opération impossible dans l'état actuel de la commande : réessayez plus tard ou consultez `GET /shop/orders/{reference}`.

#### `POST /shop/orders/{reference}/payment` : Enregistrer le paiement d'une commande

Pour une commande envoyée en `pending` puis payée sur la boutique : enregistre le paiement du reste dû. Une facture déjà soldée n'est pas payée deux fois.

Droit requis : factures (écriture).

**Paramètres**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `reference` (chemin) | texte | requis | Max 100 caractères, Caractères A-Z a-z 0-9 . _ - | Numéro de la commande dans la boutique. |
| `source` (query) | texte | non | Max 50 caractères, Défaut woocommerce | Boutique d'origine, si la commande a été envoyée avec une autre `source`. |

**Corps JSON**

| Champ | Type | Requis | Contraintes | Description |
| --- | --- | --- | --- | --- |
| `method` | texte | non | Max 60 caractères, Défaut Boutique en ligne |  |
| `transactionId` | texte | non | Max 100 caractères |  |
| `date` | date | non | Format AAAA-MM-JJ, Défaut aujourd'hui |  |

Exemple de requête :

```json
{
    "method": "Carte bancaire",
    "transactionId": "pi_3Q..."
}
```

Réponse `200` :

```json
{
    "source": "woocommerce",
    "reference": "1042",
    "status": "invoiced",
    "clientId": 57,
    "invoice": {
        "id": 318,
        "number": "FACT-318",
        "clientId": 57,
        "clientName": "Teva Exemple",
        "title": "Commande en ligne n° 1042",
        "status": "paid",
        "subtotal": 3901,
        "discount": {
            "type": null,
            "percentage": 0,
            "amount": 0
        },
        "tvaTotal": 189,
        "total": 4090,
        "createdAt": "2026-09-23T09:20:11+11:00",
        "date": "2026-09-23",
        "dueDate": "2026-09-23",
        "lines": []
    },
    "creditNote": null,
    "totals": {
        "invoice": 4090,
        "site": 4090,
        "difference": 0
    },
    "stock": [
        {
            "sku": "MIEL-500",
            "stock": 40
        }
    ],
    "paymentRecorded": true
}
```

- Erreur `404` : Aucune commande avec cette référence (et cette source).
- Erreur `403` : Module boutique non activé sur ce compte (`module_disabled`).
- Erreur `409` : Opération impossible dans l'état actuel de la commande : réessayez plus tard ou consultez `GET /shop/orders/{reference}`.
