Skip to content

Public API

An API to connect Cadanse to the rest of your tools — spreadsheets, accounting, newsletters, automation. Everything that comes out is read; what goes in is written with an idempotency key. One key per studio, one header, JSON.

The reference text below — field descriptions, parameters, error conditions — comes straight from the contract and is written in French, like the API itself. Field and code names are the ones you will see in your own responses.

The contract

The OpenAPI 3.0 document is served without a key and generated from the code that answers: it cannot describe anything other than what the API actually returns. This is what you hand to a client generator.

Open openapi.json

Getting started

Create a key from the dashboard, under Configuration. Its secret is shown once — copy it right away; Cadanse keeps only a fingerprint and cannot show it again.

Check it against the identity route, which requires no scope and returns your studio, your scopes and your remaining quota:

curl -H "Authorization: Bearer cad_..." \
  https://cadanse.app/api/v1/moi

Then read a collection. Every response carries “data” and “next”; feed “next” back as “apres” until it comes back null.

curl -H "Authorization: Bearer cad_..." \
  "https://cadanse.app/api/v1/familles?limite=100"

120 requests per minute and 20000 per day, per key. Going over returns 429 with a “Retry-After” header. The counter is closed by default: if it is unreachable, the request is refused rather than let through.

Pagination and filtering

The three parameters every collection accepts.

ParameterTypeDefault
apresCurseur exclusif : le dernier identifiant reçu à la page précédente, c'est-à-dire la valeur de « next ». Absent, la lecture commence au début.string—
limiteNombre de lignes par page. Une valeur supérieure à 200 est ramenée à 200 sans erreur ; une valeur non entière ou nulle est refusée.integer50
modifieDepuisNe rend que les lignes dont « updatedAt » est postérieur ou égal à cet instant ISO 8601. Une date invalide est REFUSÉE, jamais ignorée : un filtre qui disparaît en silence fait relire toute la collection à chaque sondage.string—

Collections

Each one requires the “<resource>:lecture” scope (for example “familles:lecture”; courses, camps, events and performances expose no family contact details). The listed fields are exhaustive: what is not there does not leave Cadanse.

/api/v1/familles

Demande la portée « familles:lecture ».

/api/v1/familles
FieldTypeNullable
idstringno
lastNamestringno
contactFirstNamestringyes
contactEmailstringno
contactPhonestringyes
addressstringyes
parent2FirstNamestringyes
parent2LastNamestringyes
parent2Emailstringyes
parent2Phonestringyes
emailOptInbooleanno
smsOptInbooleanno
emailCommercialOptOutAtstringyes
smsOptOutAtstringyes
archivedAtstringyes
mergedIntoIdstringyes
createdAtstringno
updatedAtstringno

/api/v1/eleves

Demande la portée « eleves:lecture ».

/api/v1/eleves
FieldTypeNullable
idstringno
familyIdstringno
firstNamestringno
lastNamestringno
birthDatestringyes
isAdultbooleanno
competitivebooleanno
photoConsentAtstringyes
photoConsentWithdrawnAtstringyes
createdAtstringno
updatedAtstringno

/api/v1/cours

Demande la portée « cours:lecture ».

/api/v1/cours
FieldTypeNullable
idstringno
sessionIdstringno
namestringno
descriptionstringyes
registrationEmailNotestringyes
styleIdstringno
levelIdstringyes
stream"RECREATIONAL" | "COMPETITIVE" | "BOTH" | "ADULT_RECREATIONAL" | "ADULT_COMPETITIVE" | "ADULT_BOTH"no
minAgenumberyes
maxAgenumberyes
ageCutoffMonthnumberyes
ageCutoffDaynumberyes
capacitynumberno
pricestringno
taxExemptbooleanno
instructorIdstringyes
assistantIdsstring[]no
coInstructorIdsstring[]no
roomIdstringyes
trialsOpenbooleanno
approvalRequiredbooleanno
slotsobject[]no
createdAtstringno
updatedAtstringno

/api/v1/inscriptions

Demande la portée « inscriptions:lecture ».

/api/v1/inscriptions
FieldTypeNullable
idstringno
studentIdstringno
classIdstringno
status"ENROLLED" | "WAITLISTED" | "CANCELLED" | "CART"no
enrolledAtstringno
paymentIdstringyes
ageOverridebooleanno
prereqOverridebooleanno
offeredAtstringyes
offerExpiresAtstringyes
updatedAtstringno

/api/v1/presences

Demande la portée « presences:lecture ».

/api/v1/presences
FieldTypeNullable
idstringno
enrollmentIdstringno
datestringno
status"PRESENT" | "ABSENT" | "LATE" | "EXCUSED"no
createdAtstringno
updatedAtstringno

/api/v1/paiements

Demande la portée « paiements:lecture ».

/api/v1/paiements
FieldTypeNullable
idstringno
familyIdstringno
invoiceNumberstringyes
descriptionstringyes
amountstringno
status"PENDING" | "PAID" | "REFUNDED" | "FAILED"no
method"CASH" | "INTERAC" | "CARD" | "CHEQUE" | "OTHER" | "CHEQUE_VACANCES" | "COUPON_SPORT" | "PASS_SPORT" | "PASS_CULTURE" | "CREDIT" | "BANK"yes
dueDatestringyes
paidAtstringyes
processingSincestringyes
createdAtstringno
updatedAtstringno

/api/v1/evenements

Demande la portée « evenements:lecture » ET la capacité « EVENEMENTS » en plus de l'API publique.

/api/v1/evenements
FieldTypeNullable
idstringno
sessionIdstringno
roomIdstringyes
namestringno
descriptionstringyes
startDatestringno
endDatestringno
joursstring[]no
dailyStartTimestringyes
dailyEndTimestringyes
minAgenumberyes
maxAgenumberyes
capacitynumberno
pricestringno
typesBilletsobject[]no
priceIncludesTaxbooleanno
registrationOpenbooleanno
createdAtstringno
updatedAtstringno

/api/v1/camps

Demande la portée « camps:lecture » ET la capacité « CAMPS » en plus de l'API publique.

/api/v1/camps
FieldTypeNullable
idstringno
sessionIdstringno
roomIdstringyes
namestringno
descriptionstringyes
startDatestringno
endDatestringno
dailyStartTimestringyes
dailyEndTimestringyes
minAgenumberyes
maxAgenumberyes
capacitynumberno
pricestringno
taxExemptbooleanno
registrationOpenbooleanno
createdAtstringno
updatedAtstringno

/api/v1/representations

Demande la portée « representations:lecture » ET la capacité « BILLETTERIE » en plus de l'API publique.

/api/v1/representations
FieldTypeNullable
idstringno
showIdstringno
datestringno
venuestringyes
venueRoomIdstringyes
venueMapIdstringyes
ticketPricestringyes
ticketsOnSalebooleanno
ticketsOnSaleAtstringyes
earlyTicketsOnSaleAtstringyes
maxTicketsPerFamilynumberyes
updatedAtstringno

Writing

Each write requires the “<resource>:ecriture” scope (for example “inscriptions:ecriture”), and recording a payment has its own “paiements:ecriture” scope, ticked separately. Each one goes through the same rule as the matching dashboard screen: an enrolment refused on screen is refused here, and the error code names the rule that refused it. The listed fields are the only ones accepted; an unknown field, or one the API does not read, returns 400.

The “Idempotency-Key” header is mandatory: a value of yours, unique per write. Retrying with the same key returns the original response for 24 hours, along with the “Idempotent-Replayed: true” header — that is what lets you retry without risking a second enrolment. The same key with a different body returns 409; a refused write, on the other hand, frees the key, so you can fix your payload and retry with the same one.

One creation, and the same call sent again:

curl -X POST -H "Authorization: Bearer cad_..." \
  -H "Idempotency-Key: 8f1c-..." \
  -H "Content-Type: application/json" \
  -d '{"lastName":"…","contactEmail":"…"}' \
  https://cadanse.app/api/v1/familles

POST /api/v1/familles

Demande la portée « familles:ecriture » et l'en-tête « Idempotency-Key ». La famille est créée sans élève : un élève se crée par sa propre écriture. La réponse porte la ligne au même format qu'en lecture.

POST /api/v1/familles
FieldTypeRequired
lastNamestringyes
contactEmailstringyes
contactFirstNamestringno
contactPhonestringno
addressstringno
parent2FirstNamestringno
parent2LastNamestringno
parent2Emailobjectno
parent2Phonestringno

POST /api/v1/eleves

Demande la portée « eleves:ecriture » et l'en-tête « Idempotency-Key ». L'élève est rattaché à une famille existante. Ni note médicale ni note interne : aucune des deux ne se lit par l'API, et ce qui ne se lit pas ne s'écrit pas.

POST /api/v1/eleves
FieldTypeRequired
familyIdstringyes
firstNamestringyes
lastNamestringyes
birthDatestringno
isAdultbooleanno
competitivebooleanno

POST /api/v1/inscriptions

Demande la portée « inscriptions:ecriture » et l'en-tête « Idempotency-Key ». Passe par la même fonction de domaine que le tableau de bord : capacité, conflits d'horaire, limites par participant et inscriptibilité de la session peuvent la refuser, et l'erreur nomme alors la règle. Un cours complet met l'élève en LISTE D'ATTENTE — c'est un succès, et le champ « status » de la réponse le dit. La facture de la session est émise comme elle le serait au tableau de bord.

POST /api/v1/inscriptions
FieldTypeRequired
studentIdstringyes
classIdstringyes

POST /api/v1/presences

Demande la portée « presences:ecriture » et l'en-tête « Idempotency-Key ». Marque une séance d'une inscription, aux mêmes conditions que la feuille du tableau de bord : jamais d'avance, jamais un jour de fermeture, jamais une séance annulée ou déplacée. Un statut « EXCUSED » émet un crédit de reprise quand le studio en accorde ; une transition vers « ABSENT » prévient la famille et émet l'événement « absence.signalee ». Remarquer la même séance met la marque à jour sans rien réémettre.

POST /api/v1/presences
FieldTypeRequired
enrollmentIdstringyes
datestringyes
status"PRESENT" | "ABSENT" | "LATE" | "EXCUSED"yes

POST /api/v1/paiements

Demande la portée « paiements:ecriture » et l'en-tête « Idempotency-Key ». Le montant envoyé EST le total, taxes comprises : c'est la saisie de comptoir, pas un encaissement par processeur. La facture reçoit son numéro et son empreinte fiscale, et la famille en est avertie au portail — relance courriel comprise si elle reste due.

POST /api/v1/paiements
FieldTypeRequired
familyIdstringyes
amountnumberyes
method"CASH" | "INTERAC" | "CARD" | "CHEQUE" | "OTHER" | "CHEQUE_VACANCES" | "COUPON_SPORT" | "PASS_SPORT" | "PASS_CULTURE"no
descriptionstringno
status"PENDING" | "PAID"no
dueDatestringno

Errors

Every error returns the same shape: “error.code” and “error.message”. The code is contractual; the message may be reworded.

CodeStatusWhen
MISSING_KEY401Aucun en-tête « Authorization: Bearer … » n'accompagne la requête.
INVALID_KEY401La clé est inconnue ou révoquée. Les deux cas rendent le même message.
STUDIO_SUSPENDED403Le compte du studio est suspendu.
STUDIO_ONBOARDING403La configuration initiale du studio n'est pas terminée.
PLAN_REQUIRED403Le palier du studio n'inclut pas l'API publique, ou pas la capacité que demande la collection visée.
QUOTA_UNAVAILABLE503Le compteur de quota est momentanément injoignable ; la requête est refusée plutôt que laissée passer. Réessayez après le délai de « Retry-After ».
RATE_LIMITED429Quota par minute ou quotidien dépassé. L'en-tête « Retry-After » donne le délai en secondes.
SCOPE_REQUIRED403La clé est valide mais ne porte pas la portée qu'exige la route.
INVALID_PARAMETER400Un paramètre de requête est mal formé — « limite » non entière ou nulle, « modifieDepuis » qui n'est pas une date ISO 8601.
IDEMPOTENCY_KEY_REQUIRED400L'en-tête « Idempotency-Key » manque, est vide, ou dépasse 255 caractères. Il est obligatoire sur toute écriture : un connecteur qui réessaie est le cas normal, pas l'exception.
IDEMPOTENCY_KEY_REUSED409Cette clé a déjà servi, pour une requête DIFFÉRENTE. Rendre la réponse d'origine ferait croire à l'appelant qu'il vient de créer ce que décrit une réponse qui parle d'autre chose. Une clé vaut pour une écriture et une seule.
IDEMPOTENCY_IN_PROGRESS409Une écriture portant cette clé court encore. Réessayez après le délai de « Retry-After » : la réponse d'origine sera alors rendue.
INVALID_BODY400Le corps n'est pas du JSON valide, ou un champ ne satisfait pas le schéma de l'écriture. Le message nomme le champ fautif. Le refus ne consomme PAS la clé d'idempotence : corrigez et réessayez avec la même.
PAYLOAD_TOO_LARGE413Le corps de la requête dépasse la taille permise ; il est refusé avant tout décodage. Réduisez-le (par lots plus petits) avant de réessayer.
FAMILY_NOT_FOUND404Le « familyId » visé n'existe pas dans ce studio.
STUDENT_NOT_FOUND404Le « studentId » visé n'existe pas dans ce studio.
CLASS_NOT_FOUND404Le « classId » visé n'existe pas dans ce studio.
ENROLLMENT_NOT_FOUND404L'« enrollmentId » visé n'existe pas dans ce studio.
ALREADY_ENROLLED409Cet élève est déjà inscrit à ce cours. Réinscrire ne crée pas de seconde ligne — le refus est ce qui empêche une double facturation.
SESSION_NOT_ENROLLABLE409La session du cours n'accepte pas d'inscription : inactive, terminée, ou dont la période est passée.
SCHEDULE_CONFLICT409L'élève a déjà un cours qui chevauche celui-ci. Seul le studio peut assumer un conflit d'horaire, depuis son tableau de bord : l'API ne porte pas cette dérogation.
PARTICIPANT_LIMIT409L'inscription dépasse une limite fixée par le studio — nombre de cours par élève, ou un seul cours par discipline.
ATTENDANCE_DATE_IN_FUTURE409La date de la présence est encore à venir. Une présence ne se prend pas d'avance : elle nourrit le décompte de séances tenues et l'avis d'absence à la famille.
STUDIO_CLOSED409Le studio est fermé ce jour-là : aucune séance n'y a lieu.
SESSION_NOT_HELD409La séance de ce cours a été annulée ou déplacée : elle n'a pas eu lieu à la date demandée. Le message dit lequel des deux.
METHOD_NOT_OFFERED422Le moyen de paiement n'existe pas dans le pays du studio : `INTERAC` est propre au Canada, et `BANK` (compte bancaire débité) au Canada (débit préautorisé), aux États-Unis (ACH) et en France (SEPA) ; les Chèques-Vacances, coupons sport, Pass'Sport et Pass Culture sont propres à la France.
INTERNAL_ERROR500Erreur interne. Le détail est consigné côté Cadanse, jamais rendu à l'appelant.

Outgoing webhooks

The API is read and written at your initiative; webhooks come to you. A destination is set up from the dashboard, under Configuration: an HTTPS address, the subscribed events, and a signing secret shown once. Cadanse POSTs “application/json” there on every event.

Verifying the signature

Every request carries the “Cadanse-Signature” header, shaped “t=<milliseconds>,v1=<hex>”: “v1” is the HMAC-SHA256 of the text “<t>.<raw body>”, keyed with your secret. Verify the RAW body, before any deserialization. Check the signature FIRST and the timestamp second: rejecting on age before proving authenticity tells whoever is writing to you which clock you accept. Past 5 minutes, refuse — that is what stops a body captured once from being replayed at your end. The “Cadanse-Event” header names the event, so you can route without reading the body.

Cadanse-Signature: t=1776171600000,v1=<hex>
Cadanse-Event: inscription.creee

The envelope

Always the same shape: “id”, “type”, “createdAt”, “studioId” and “data”. “id” is the DELIVERY's, not the object's: retries of one delivery repeat it — that is what lets you deduplicate — and a manually replayed delivery carries a new one. “createdAt” is the moment of the event; no payload carries a second timestamp. The fields of “data” are identifiers: full records are read from the collections above.

Retries

A 2xx response closes the delivery. Otherwise 3 retries, at 1 h, 6 h, 24 h, then nothing: an event delivered three days late informs nobody. The whole window is 48 h. Answer quickly — a call is abandoned past 2 seconds — and do your work after answering. The dashboard shows every delivery, its response code and your server's error message; an exhausted delivery can be replayed by hand from there.

The events

Subscribing to an event that sends nothing yet is allowed, and the right-hand column says so: it is the real state, read from the code that emits.

The events
EventFields of “data”State
inscription.creeeinscriptionId, eleveId, coursId, familleId, statut, paiementIdsent
paiement.recupaiementId, familleId, montant, moyen, numeroFacture, payeLesent
absence.signaleeinscriptionId, eleveId, coursId, date, statutsent
famille.creeefamilleId, nom, courrielsent
billet.venducommandeId, representationId, familleId, quantite, surLesGenoux, paiementId, venduLesent

Connectors

Zapier, Make and assistants that speak MCP plug into everything above, and into nothing else: the same key, the same scopes, the same quota, the same log. A connector does not open one more door — it walks through a door you already opened, and the key can be revoked from the dashboard.

Zapier and Make

Nothing to install: create an “API key” connection with the “Authorization” header and, as its value, “Bearer” followed by your key. Test it against https://cadanse.app/api/v1/moi — that route answers whatever the key's scopes are, and returns the studio name and the list of what the key allows.

Your triggers are the webhooks above. Both Zapier and Make hand you a listening URL: paste it into the dashboard and tick the events you want. Neither verifies the signature for you — have a code step verify it before acting on a payload, using the recipe given above.

Your actions are the writes above, called from an “HTTP request” step. Always fill in “Idempotency-Key”: otherwise a scenario the platform replays after an outage would create the same record twice.

MCP server

An assistant that speaks MCP connects to https://cadanse.app/api/v1/mcp — HTTP transport, POST requests, the same “Authorization” header. Your studio decides: connecting an assistant sends data out to a tool you choose, under a key you create and can revoke.

{
  "mcpServers": {
    "cadanse": {
      "type": "http",
      "url": "https://cadanse.app/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer cad_..."
      }
    }
  }
}

The server exposes 9 read tools and 5 write tools — exactly the collections and writes documented above, and no others. A tool is named after its operation, in lowercase with underscores:

lister_familles, lister_eleves, lister_cours, lister_inscriptions, lister_presences, lister_paiements, lister_evenements, lister_camps, lister_representations, creer_famille, creer_eleve, creer_inscription, marquer_presence, creer_paiement

The catalogue depends on the key: “tools/list” returns only the tools its scopes cover. A tool outside those scopes does not exist for that key — calling it answers “unknown tool”, which avoids teaching a prober which scopes exist.

JSON-RPC batches are refused: one call is one request, and one request is one unit of quota. Reads made over MCP are logged like the others — one window per key, per collection, per day — in the same log the studio can read back.

Write tools act immediately, and what they write is visible to staff and families. Each accepts an optional “cleIdempotence” argument: without it, two identical calls make two writes — deliberately, since two cash payments of the same amount on the same Saturday are two payments. The audit log records every write, whether it came from an assistant or from an HTTP call.

Why this page cannot lie

It is rendered from the same document as openapi.json, which is itself generated from the serializers that write the responses. A field removed from the code disappears from here with nobody thinking about it; a documented field that is not served cannot be written.