Les autres guides déroulent des parcours ; celui-ci rassemble les règles qui s'appliquent à chaque appel, à garder sous la main pendant que vous écrivez votre client.
BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...1. L'enveloppe data
Toute réponse réussie enveloppe sa charge utile dans data :
{ "data": [ ... ] }Deux exceptions sans corps : 204 No Content sur les suppressions et sur quelques actions (PATCH …/payslips/{payslipId}, POST …/payslips/{payslipId}/send, POST …/quotes/{quoteId}/send, POST …/invoices/{invoiceId}/cancel-payment), et 302 sur les téléchargements de PDF, de justificatifs et de logos, à suivre avec -L.
2. Pagination
Les listes des ressources de société sont paginées, du plus récent au plus ancien (les employés suivent leur ordre de création) :
{ "data": [ ... ], "nextCursor": "..." }Quelques listes ne sont pas paginées et répondent un tableau brut sous data : GET /companies, GET /mandates, GET /companies/{companyId}/invoice-templates, GET …/payroll/holiday-variants et GET …/employees/{employeeId}/payslips-to-update. POST …/payslips/pdf-batch est paginé lui aussi, mais son curseur voyage dans le corps, en cursor.
3. Dates et identifiants
Les dates sont acceptées en millisecondes epoch ou en YYYY-MM-DD, et renvoyées en millisecondes epoch. Les filtres from/to bornent la date d'émission, bornes incluses. Deux exceptions : les absences et les dates d'un employé (startDate, endDate, dateOfBirth) sont des chaînes YYYY-MM-DD, à l'aller comme au retour.
Les identifiants sont des chaînes opaques. Dans le chemin, un identifiant inconnu, un identifiant malformé et un identifiant d'une autre société répondent tous 404 NOT_FOUND : les trois cas sont volontairement indiscernables, pour qu'aucun identifiant d'un autre compte ne fuite. Dans le corps, une référence à une autre société répond selon la route : 403 ACCESS_DENIED sur une facture ou un devis, 422 CLIENT_NOT_FOUND ou 422 TEMPLATE_NOT_FOUND sur une récurrence, 422 EMPLOYEE_NOT_FOUND sur une absence.
4. Corps stricts
- Les corps JSON sont stricts : toute clé inconnue répond
400 VALIDATION_ERROR. Sur la création et la modification d'un employé et sur la création d'une fiche de salaire, le refus vient du contrôle côté serveur et nomme le champ dansmessage, sansissues. Les corps de…/laa-periodset…/avs-choices, eux, ignorent les clés inconnues. - Un corps est toujours requis sur
POST,PUTetPATCH({}est un patch vide valide ; un corps vide estINVALID_JSON), sauf sur les actions sans corps commePOST …/default,…/convert,…/cancel-paymentou…/payslips/{payslipId}/send;DELETEn'en prend pas. Attention,POST …/payslips/{payslipId}/validateattend au moins{}. - Au-delà de 1 Mo, le corps est refusé :
413 PAYLOAD_TOO_LARGE. - Sept routes prennent des octets bruts plutôt qu'un JSON.
POST …/expenses/{expenseId}/document,POST …/expense-drafts/extract,POST …/uploads/expenseetPOST …/uploads/invoice-import: PDF, PNG, JPEG ou WebP, 10 Mo.POST …/uploads/invoice-source: PNG, JPEG ou WebP, 8 Mo.PUT …/logosur une société et sur un modèle de facture : PNG ou JPEG seulement, 2 Mo. LeContent-Typeannonce le format, le corps est le fichier ; tout le reste passe par la requête. Un autre format répond415 UNSUPPORTED_MEDIA_TYPE, un corps vide400 EMPTY_BODY. - Un téléversement
…/uploads/…renvoie unuploadIdvalable une heure. SeulPOST …/expense-draftsen consomme un aujourd'hui : aucune route publique n'accepte ceux deinvoice-sourceetinvoice-import. - Une clé de requête inconnue est un
400 VALIDATION_ERRORsur toute route qui déclare un contrat de requête ; les autres, commeGET /meetGET /companies, ignorent le reste, sans que ce soit une promesse. PUTsur une absence est un remplacement complet : les champs facultatifs omis sont effacés.PUTsur une facture ou un devis remplace toutes les lignes et demandeexpectedVersion.
Un échec de validation liste chaque champ fautif sous issues, en chemins pointés, index de tableau compris :
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request body",
"issues": [
{ "path": "subtotal", "message": "Unrecognized key \"subtotal\"" },
{ "path": "vatRates.0.rate", "message": "Too big: expected number to be <=100" }
]
}
}5. Erreurs
Les erreurs portent un code stable, lisible par une machine :
{ "error": { "code": "PAYSLIP_ALREADY_EXISTS" } }VALIDATION_ERROR ajoute issues, ou un message pour les champs contrôlés côté serveur. INVALID_TRANSITION, BATCH_SIZE_INVALID et UNSUPPORTED_MEDIA_TYPE ajoutent aussi un message, et PAYROLL_CUMULATIVE_INCOMPLETE un details.months ; tout autre code répond seul. Branchez-vous sur error.code, jamais sur la prose. Cette enveloppe vaut sur tout chemin sous /api/v1/, y compris un chemin inconnu (404 NOT_FOUND) et une méthode non supportée (405 METHOD_NOT_ALLOWED, avec un en-tête Allow).
Les contrôles tournent dans cet ordre : chemin et méthode (404, 405), taille du corps (413), clé et propriétaire (401), visibilité de la société (404), puis société archivéeplanmodulepermission (403). Viennent ensuite la validation du corps et de la requête (400), puis les règles métier. Une société dont le porteur de clé n'est pas membre est indiscernable d'une société inexistante.
400 INVALID_JSON,400 VALIDATION_ERROR: corps absent ou illisible, champ invalide.400 BATCH_SIZE_INVALID:payslips/batchoubank-transactions/batchhors de 1 à 100 éléments ; les autres lots sont bornés par leur schéma et répondentVALIDATION_ERROR.401 INVALID_API_KEY: clé absente, malformée, inconnue, révoquée ou expirée ; propriétaire banni ou supprimé.402 SUBSCRIPTION_REQUIRED: création d'un mandat sans abonnement fiduciaire actif.403 PLAN_NOT_ALLOWED,403 COMPANY_ARCHIVED,403 ACCESS_DENIED,403 ACCOUNT_DISABLED: plan, archivage, permission ou rôle manquants, compte désactivé.403 INVOICING_MODULE_DISABLED,403 PAYROLL_MODULE_DISABLED,403 ACCOUNTING_MODULE_DISABLED,403 PRODUCT_CATALOG_DISABLED: module ou catalogue désactivé sur la société.403 LEDGER_DISABLED,403 FINANCIAL_STATEMENTS_REPORTING_DISABLED: fonction fermée sur le déploiement.403 *_LIMIT_REACHED: quota du plan atteint (clients, produits, listes de produits, factures, récurrences, employés, fiches, dépenses).403 EXPENSE_AI_QUOTA_REACHED,403 AI_QUOTA_REACHED: quotas IA.403 CURRENCY_PRO_ONLY: devise autre que CHF aux limites Starter.403 FILE_ACCESS_DENIED,403 UPLOAD_EXPIRED:uploadIdd'un autre utilisateur, déjà utilisé ou expiré.409 INVALID_TRANSITION,409 PAYSLIP_ALREADY_EXISTS,409 PAYSLIP_DECLARED,409 ABSENCE_OVERLAPS_EXISTING,409 QUOTE_NOT_EDITABLE,409 BILL_PAID_LOCKED,409 BILL_NOT_PAID,409 DOCUMENT_ALREADY_ATTACHED,409 EXPENSE_DRAFT_LIMIT_REACHED: conflit avec l'état actuel.409 BILL_VERSION_CONFLICT,409 QUOTE_VERSION_CONFLICT: le document a changé depuis votre lecture.422 MODIFICATION_REASON_REQUIRED,422 REASON_REQUIRED,422 CLIENT_EMAIL_MISSING,422 EMPLOYEE_EMAIL_MISSING,422 CLASSIFICATION_ACCOUNT_REQUIRED,422 VALID_UNTIL_IN_PAST,422 SCAN_UNREADABLE: requête valide mais irrecevable dans ce contexte.429 AI_RATE_LIMITED: le modèle d'IA limite les appels, réessayez.429 MANDATE_REQUEST_LIMIT_REACHED: trop de demandes de mandat.503 PDF_UNAVAILABLE,502 EMAIL_SEND_FAILED,502 AI_REQUEST_FAILED,500 INTERNAL_ERROR: réessayez. Une seule exception : après502 QUOTE_EMAIL_SENT_TRANSITION_FAILED, le devis est déjà parti, ne le renvoyez pas.
La Swagger UI et openapi.json de votre déploiement publient les codes déclarés, route par route. D'autres codes métier restent possibles : prévoyez toujours un cas par défaut sur error.code.
6. Limites et utilisation
Il n'y a pas encore de limitation de débit. Les appels sont comptés, pas freinés : un compteur par clé, endpoint, société visée et jour UTC, visible dans Mon compteClés APIUtilisation. Les requêtes rejetées par l'authentification ne sont pas comptées ; un 400 sur un appel autorisé l'est.
Soyez un bon voisin malgré tout : paginez les listes plutôt que de les interroger en boucle, et reculez sur 500 ou 503. Les lots ne se comportent pas tous pareil. POST …/payslips/batch n'est pas atomique et rend un résultat par élément. POST …/bank-transactions/batch, POST …/clients/batch et POST …/{invoices,quotes,payslips}/bulk-delete sont atomiques, tout ou rien. Les routes bulk-* des dépenses et des transactions bancaires, et …/expense-drafts/accept et …/reject, traitent ce qu'elles peuvent et comptent le reste dans skipped.
7. Sandbox et réinitialisation
Le sandbox n'écrit jamais en production. E-mails, Stripe et invitations d'équipe y sont inertes, l'inscription est fermée, les logos ne sont pas copiés. Tout le reste tourne, y compris les tâches planifiées : les factures récurrentes y sont générées et les fiches de salaire calculées sur les données copiées. Le grand livre y est ouvert, contrairement à la production, où ses routes répondent 403 LEDGER_DISABLED.
Retour aux parcours : Démarrer avec l'API, Facturer par l'API, Paie, dépenses et comptabilité par l'API, Cycle de paie par l'API. Chaque champ et chaque réponse sont dans la Swagger UI de votre déploiement (…/api/docs).