Aller au contenu principal

Documentation API

Lisez vos factures depuis votre comptabilité, créez vos clients depuis votre CRM.

Spécification OpenAPI 3.1Créer une clé

Authentification

Chaque requête porte une clé d’API, créée depuis vos paramètres. Une clé identifie un espace, pas une personne, et ne peut faire que ce que ses portées autorisent.

curl https://syllodi-digital.fr/api/v1/invoices \
  -H "Authorization: Bearer sd_live_a1b2c3d4e5f6_…"

La clé n’est affichée qu’une seule fois, à sa création. Nous n’en conservons qu’une empreinte : pouvoir la réafficher signifierait la stocker en clair.

Les montants sont en centimes

Une facture de 1 320 € renvoie 132000, et non « 1320.00 ».

Ce n’est pas un détail. Les nombres à virgule flottante ne représentent pas exactement les décimales : 0.1 + 0.2 ne vaut pas 0.3. Sur une facture, cet écart devient une erreur comptable. Le centime entier ne souffre d’aucune ambiguïté.

"amounts": {
  "total_excl_tax": 120000,   // 1 200,00 €
  "total_tax":       12000,   //   120,00 €
  "total_incl_tax": 132000,   // 1 320,00 €
  "formatted_total": "1 320,00 €"
}

Portées

N’accordez à chaque clé que ce dont elle a besoin.

customers:read
Lire la liste et les fiches clients
customers:write
Créer et modifier des clients
quotes:read
Lire les devis
quotes:write
Créer des devis
invoices:read
Lire les factures et les règlements
invoices:write
Créer des factures et enregistrer des règlements

Pagination

Par curseur, pas par numéro de page : sur une liste qui bouge entre deux appels, la page 2 peut sauter ou répéter des éléments.

# Première page
GET /api/v1/invoices?limit=50

{ "data": [...], "has_more": true, "next_cursor": "8f3a…" }

# Suivante — on suit le curseur tant que has_more vaut true
GET /api/v1/invoices?limit=50&cursor=8f3a…

Points d’entrée

GET/api/v1/customerscustomers:read

Liste des clients. Paramètres : limit (1 à 100), cursor, search (nom, référence ou e-mail).

POST/api/v1/customerscustomers:write

Crée un client. Seul le nom est obligatoire.

curl -X POST https://syllodi-digital.fr/api/v1/customers \
  -H "Authorization: Bearer sd_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "COMPANY",
    "name": "PLOMBERIE MARTIN",
    "email": "contact@exemple.fr",
    "siret": "79234156700018"
  }'

GET/api/v1/customers/{id}customers:read

Détail d’un client, avec ses statistiques : nombre de devis et de factures, total facturé, total réglé, encours.

GET/api/v1/invoicesinvoices:read

Liste des factures. Filtres : status, issued_after, issued_before.

# Les factures réglées de septembre
GET /api/v1/invoices?status=PAID&issued_after=2026-09-01&issued_before=2026-09-30

GET/api/v1/invoices/{id}invoices:read

Détail d’une facture, lignes comprises, avec son bloc integrity : l’empreinte qui permet de vérifier qu’elle n’a pas été modifiée depuis son émission.

Ce que l’API ne fait pas

Elle n’émet pas de factures. Émettre attribue un numéro légal définitif et scelle le document de façon irréversible. Un script rejoué deux fois créerait des numéros orphelins dans une séquence que la loi impose de garder continue (art. 242 nonies A de l’annexe II au CGI).

L’émission passe par l’interface, où elle est confirmée explicitement. L’usage réel de cette API est la lecture : synchroniser une comptabilité, alimenter un tableau de bord, rapprocher des règlements.

Notifications sortantes (webhooks)

Plutôt que d’interroger l’API en boucle pour savoir si un devis a été signé, vous pouvez nous demander de prévenir votre outil. Chaque événement déclenche un POST vers l’adresse de votre choix, avec le contenu en JSON.

Vérifier que le message vient bien de nous

Chaque appel porte un en-tête x-syllodi-signature de la forme t=1789…,v1=a3f…. Recalculez l’empreinte et comparez :

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifier(corpsBrut, entete, secret) {
  const champs = Object.fromEntries(
    entete.split(',').map((p) => p.trim().split('=')),
  );
  const horodatage = Number(champs.t);

  // L'écart est pris en valeur absolue : un horodatage dans le FUTUR
  // doit être refusé lui aussi.
  if (Math.abs(Date.now() / 1000 - horodatage) > 300) return false;

  const attendu = createHmac('sha256', secret)
    .update(`${horodatage}.${corpsBrut}`)
    .digest('hex');

  if (champs.v1.length !== attendu.length) return false;
  return timingSafeEqual(Buffer.from(champs.v1), Buffer.from(attendu));
}

Signez le corps brut, tel que reçu. Le reformater — même en réordonnant des clés — change l’empreinte et fait échouer la vérification.

Ce que nous attendons de votre réponse

  • Répondez 2xx dès réception, avant tout traitement long. Au-delà de dix secondes sans réponse, l’envoi est considéré comme échoué.
  • Une réponse 5xx, un délai dépassé, un 429 ou un 408 déclenchent de nouveaux essais espacés (1 min, 5 min, 30 min, 2 h, 6 h, 24 h).
  • Toute autre réponse 4xx est comprise comme un refus définitif : aucun nouvel essai.
  • Après dix envois consécutifs sans succès, la destination est désactivée. Vous pouvez la réactiver depuis vos réglages.

Le même événement peut arriver deux fois

C’est le propre d’un système qui réessaie : votre réponse peut s’être perdue après que vous avez traité le message. Utilisez l’identifiant de l’en-tête x-syllodi-delivery — stable d’un essai à l’autre — pour ignorer un doublon.

Contraintes sur l’adresse

Elle doit être en https et publiquement joignable. Les adresses de réseau privé sont refusées, et les redirections ne sont pas suivies : sans cela, une adresse pourrait servir à faire appeler, par nos serveurs, des machines internes.

Limites d’appel

L’API accepte 120 appels par minute et par clé. Une requête sans clé valable est plus sévèrement bornée — 20 par minute et par adresse — parce qu’il s’agit alors d’un essai à l’aveugle.

Chaque réponse porte l’état de votre quota :

ratelimit-limit: 120
ratelimit-remaining: 117
ratelimit-reset: 43

Au-delà, la réponse est un 429 accompagné d’un en-tête retry-after en secondes. Respectez-le : un appel refusé compte quand même, donc insister prolonge l’attente.

La fenêtre est glissante. Un quota épuisé ne se rouvre pas d’un coup à la minute suivante : il se libère progressivement.

Erreurs

{ "error": { "code": "insufficient_scope", "message": "…" } }
401
missing_credentialsEn-tête Authorization absent
401
invalid_keyClé inconnue, révoquée ou incorrecte
401
expired_keyClé arrivée à échéance
403
insufficient_scopeLa clé n’a pas la portée demandée
403
workspace_suspendedL’espace est suspendu
404
not_foundRessource inexistante — ou appartenant à un autre espace
422
validation_failedDonnées invalides (le détail est renvoyé)

Une ressource appartenant à un autre espace répond 404 et non 403 : répondre « interdit » confirmerait son existence.