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.
| Parameter | Type | Default |
|---|
| 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. | integer | 50 |
|---|
| 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| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| lastName | string | no |
|---|
| contactFirstName | string | yes |
|---|
| contactEmail | string | no |
|---|
| contactPhone | string | yes |
|---|
| address | string | yes |
|---|
| parent2FirstName | string | yes |
|---|
| parent2LastName | string | yes |
|---|
| parent2Email | string | yes |
|---|
| parent2Phone | string | yes |
|---|
| emailOptIn | boolean | no |
|---|
| smsOptIn | boolean | no |
|---|
| emailCommercialOptOutAt | string | yes |
|---|
| smsOptOutAt | string | yes |
|---|
| archivedAt | string | yes |
|---|
| mergedIntoId | string | yes |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/eleves
Demande la portée « eleves:lecture ».
/api/v1/eleves| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| familyId | string | no |
|---|
| firstName | string | no |
|---|
| lastName | string | no |
|---|
| birthDate | string | yes |
|---|
| isAdult | boolean | no |
|---|
| competitive | boolean | no |
|---|
| photoConsentAt | string | yes |
|---|
| photoConsentWithdrawnAt | string | yes |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/cours
Demande la portée « cours:lecture ».
/api/v1/cours| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| sessionId | string | no |
|---|
| name | string | no |
|---|
| description | string | yes |
|---|
| registrationEmailNote | string | yes |
|---|
| styleId | string | no |
|---|
| levelId | string | yes |
|---|
| stream | "RECREATIONAL" | "COMPETITIVE" | "BOTH" | "ADULT_RECREATIONAL" | "ADULT_COMPETITIVE" | "ADULT_BOTH" | no |
|---|
| minAge | number | yes |
|---|
| maxAge | number | yes |
|---|
| ageCutoffMonth | number | yes |
|---|
| ageCutoffDay | number | yes |
|---|
| capacity | number | no |
|---|
| price | string | no |
|---|
| taxExempt | boolean | no |
|---|
| instructorId | string | yes |
|---|
| assistantIds | string[] | no |
|---|
| coInstructorIds | string[] | no |
|---|
| roomId | string | yes |
|---|
| trialsOpen | boolean | no |
|---|
| approvalRequired | boolean | no |
|---|
| slots | object[] | no |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/inscriptions
Demande la portée « inscriptions:lecture ».
/api/v1/inscriptions| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| studentId | string | no |
|---|
| classId | string | no |
|---|
| status | "ENROLLED" | "WAITLISTED" | "CANCELLED" | "CART" | no |
|---|
| enrolledAt | string | no |
|---|
| paymentId | string | yes |
|---|
| ageOverride | boolean | no |
|---|
| prereqOverride | boolean | no |
|---|
| offeredAt | string | yes |
|---|
| offerExpiresAt | string | yes |
|---|
| updatedAt | string | no |
|---|
/api/v1/presences
Demande la portée « presences:lecture ».
/api/v1/presences| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| enrollmentId | string | no |
|---|
| date | string | no |
|---|
| status | "PRESENT" | "ABSENT" | "LATE" | "EXCUSED" | no |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/paiements
Demande la portée « paiements:lecture ».
/api/v1/paiements| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| familyId | string | no |
|---|
| invoiceNumber | string | yes |
|---|
| description | string | yes |
|---|
| amount | string | no |
|---|
| status | "PENDING" | "PAID" | "REFUNDED" | "FAILED" | no |
|---|
| method | "CASH" | "INTERAC" | "CARD" | "CHEQUE" | "OTHER" | "CHEQUE_VACANCES" | "COUPON_SPORT" | "PASS_SPORT" | "PASS_CULTURE" | "CREDIT" | "BANK" | yes |
|---|
| dueDate | string | yes |
|---|
| paidAt | string | yes |
|---|
| processingSince | string | yes |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/evenements
Demande la portée « evenements:lecture » ET la capacité « EVENEMENTS » en plus de l'API publique.
/api/v1/evenements| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| sessionId | string | no |
|---|
| roomId | string | yes |
|---|
| name | string | no |
|---|
| description | string | yes |
|---|
| startDate | string | no |
|---|
| endDate | string | no |
|---|
| jours | string[] | no |
|---|
| dailyStartTime | string | yes |
|---|
| dailyEndTime | string | yes |
|---|
| minAge | number | yes |
|---|
| maxAge | number | yes |
|---|
| capacity | number | no |
|---|
| price | string | no |
|---|
| typesBillets | object[] | no |
|---|
| priceIncludesTax | boolean | no |
|---|
| registrationOpen | boolean | no |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/camps
Demande la portée « camps:lecture » ET la capacité « CAMPS » en plus de l'API publique.
/api/v1/camps| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| sessionId | string | no |
|---|
| roomId | string | yes |
|---|
| name | string | no |
|---|
| description | string | yes |
|---|
| startDate | string | no |
|---|
| endDate | string | no |
|---|
| dailyStartTime | string | yes |
|---|
| dailyEndTime | string | yes |
|---|
| minAge | number | yes |
|---|
| maxAge | number | yes |
|---|
| capacity | number | no |
|---|
| price | string | no |
|---|
| taxExempt | boolean | no |
|---|
| registrationOpen | boolean | no |
|---|
| createdAt | string | no |
|---|
| updatedAt | string | no |
|---|
/api/v1/representations
Demande la portée « representations:lecture » ET la capacité « BILLETTERIE » en plus de l'API publique.
/api/v1/representations| Field | Type | Nullable |
|---|
| id | string | no |
|---|
| showId | string | no |
|---|
| date | string | no |
|---|
| venue | string | yes |
|---|
| venueRoomId | string | yes |
|---|
| venueMapId | string | yes |
|---|
| ticketPrice | string | yes |
|---|
| ticketsOnSale | boolean | no |
|---|
| ticketsOnSaleAt | string | yes |
|---|
| earlyTicketsOnSaleAt | string | yes |
|---|
| maxTicketsPerFamily | number | yes |
|---|
| updatedAt | string | no |
|---|
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| Field | Type | Required |
|---|
| lastName | string | yes |
|---|
| contactEmail | string | yes |
|---|
| contactFirstName | string | no |
|---|
| contactPhone | string | no |
|---|
| address | string | no |
|---|
| parent2FirstName | string | no |
|---|
| parent2LastName | string | no |
|---|
| parent2Email | object | no |
|---|
| parent2Phone | string | no |
|---|
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| Field | Type | Required |
|---|
| familyId | string | yes |
|---|
| firstName | string | yes |
|---|
| lastName | string | yes |
|---|
| birthDate | string | no |
|---|
| isAdult | boolean | no |
|---|
| competitive | boolean | no |
|---|
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| Field | Type | Required |
|---|
| studentId | string | yes |
|---|
| classId | string | yes |
|---|
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| Field | Type | Required |
|---|
| enrollmentId | string | yes |
|---|
| date | string | yes |
|---|
| 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| Field | Type | Required |
|---|
| familyId | string | yes |
|---|
| amount | number | yes |
|---|
| method | "CASH" | "INTERAC" | "CARD" | "CHEQUE" | "OTHER" | "CHEQUE_VACANCES" | "COUPON_SPORT" | "PASS_SPORT" | "PASS_CULTURE" | no |
|---|
| description | string | no |
|---|
| status | "PENDING" | "PAID" | no |
|---|
| dueDate | string | no |
|---|
Errors
Every error returns the same shape: “error.code” and “error.message”. The code is contractual; the message may be reworded.
| Code | Status | When |
|---|
| MISSING_KEY | 401 | Aucun en-tête « Authorization: Bearer … » n'accompagne la requête. |
|---|
| INVALID_KEY | 401 | La clé est inconnue ou révoquée. Les deux cas rendent le même message. |
|---|
| STUDIO_SUSPENDED | 403 | Le compte du studio est suspendu. |
|---|
| STUDIO_ONBOARDING | 403 | La configuration initiale du studio n'est pas terminée. |
|---|
| PLAN_REQUIRED | 403 | Le palier du studio n'inclut pas l'API publique, ou pas la capacité que demande la collection visée. |
|---|
| QUOTA_UNAVAILABLE | 503 | Le 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_LIMITED | 429 | Quota par minute ou quotidien dépassé. L'en-tête « Retry-After » donne le délai en secondes. |
|---|
| SCOPE_REQUIRED | 403 | La clé est valide mais ne porte pas la portée qu'exige la route. |
|---|
| INVALID_PARAMETER | 400 | Un 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_REQUIRED | 400 | L'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_REUSED | 409 | Cette 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_PROGRESS | 409 | Une é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_BODY | 400 | Le 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_LARGE | 413 | Le 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_FOUND | 404 | Le « familyId » visé n'existe pas dans ce studio. |
|---|
| STUDENT_NOT_FOUND | 404 | Le « studentId » visé n'existe pas dans ce studio. |
|---|
| CLASS_NOT_FOUND | 404 | Le « classId » visé n'existe pas dans ce studio. |
|---|
| ENROLLMENT_NOT_FOUND | 404 | L'« enrollmentId » visé n'existe pas dans ce studio. |
|---|
| ALREADY_ENROLLED | 409 | Cet é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_ENROLLABLE | 409 | La session du cours n'accepte pas d'inscription : inactive, terminée, ou dont la période est passée. |
|---|
| SCHEDULE_CONFLICT | 409 | L'é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_LIMIT | 409 | L'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_FUTURE | 409 | La 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_CLOSED | 409 | Le studio est fermé ce jour-là : aucune séance n'y a lieu. |
|---|
| SESSION_NOT_HELD | 409 | La 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_OFFERED | 422 | Le 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_ERROR | 500 | Erreur 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| Event | Fields of “data” | State |
|---|
| inscription.creee | inscriptionId, eleveId, coursId, familleId, statut, paiementId | sent |
|---|
| paiement.recu | paiementId, familleId, montant, moyen, numeroFacture, payeLe | sent |
|---|
| absence.signalee | inscriptionId, eleveId, coursId, date, statut | sent |
|---|
| famille.creee | familleId, nom, courriel | sent |
|---|
| billet.vendu | commandeId, representationId, familleId, quantite, surLesGenoux, paiementId, venduLe | sent |
|---|
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.