Changelog
3.19.0
October 25, 2026
Menu item labels are now limited to 50 characters. No operation, event, or error code was added or removed.
Changed
labelinPOST /menus/{menu_key}/itemsandPATCH /menus/{menu_key}/items/{menu_item_id}now accepts at most 50 characters (it was 155). A longer label isvalidation_failedon thelabelfield. Items saved with a longer label before this release keep it and are still returned as they are; only a new write must fit the limit. Integrations that send labels longer than 50 characters must shorten them.
3.18.0
October 22, 2026
The configuration manifest now covers every settings write, and a new policy template groups the settings permissions. No operation, event, or error code was added or removed.
Added
kind: email_templatein the configuration manifest: updates an email template, withidset to the template ID (sale,import,free, ornew_access) andattributesin the request body format ofPATCH /settings/emails/templates/{email_template_id}. Templates are a fixed catalog, so the manifest only updates them.kind: custom_fieldin the configuration manifest: creates, updates, and deletes profile custom fields, in the request body format ofPOSTandPATCH /settings/user-profile/custom-fields.settings_managerinGET /credentials/policy-templates: everysettings.update*permission, custom code, profile custom fields, and the configuration manifest (configuration.validate,configuration.plan,configuration.apply).
Changed
- A
settingsitem with an unknownidis stillvalidation_failed, and the message now lists the accepted settings groups and points tokind: email_templateandkind: custom_field. - The
kindenum ofConfigurationManifestand theidenum ofPolicyTemplateListare now marked extensible (x-extensible): new values can appear in a minor release.
3.17.0
October 21, 2026
Adds the platform time zone to the platform settings. No operation, event, or error code was added or removed.
Added
timezoneinGET /settings/platformandPATCH /settings/platform: the IANA time zone of the platform, such asAmerica/Sao_Paulo(the default when it was never set). It sets how dates are shown to students and admins and how business dates without an offset are read; instants already stored do not change. Sendingnullreturns the platform to the default.
3.16.0
October 19, 2026
Removes the anonymous block from support tickets. No operation, event, or error code was added or removed.
Changes that may require action
anonymousis no longer part of theTicketschema (GET /support/tickets,GET /support/tickets/{ticket_id}and the expandedticketof webhooks). No ticket was ever stored with the anonymous contact fields, so no response returned the block: a ticket without a user keepsuser: null.
3.15.4
October 18, 2026
Fixes two answers of the points ledger. No operation, field, event, or error code was added, removed, or changed.
Fixes
GET /gamification/scoresno longer lists the entries of deleted users, as the routes under/users/{user_id}already did. The entries are kept and come back if the user is restored.cause.lesson_idofcommentandquestionentries is returned only when the entry references the lesson. Comments created before June 3, 2026 and questions created before April 14, 2026 referenced the message instead, andlesson_idcould point to an unrelated lesson; those entries now returncause.kindwithoutlesson_id.
3.15.3
October 16, 2026
Fixes the resource_id filter of GET /events and the visual settings of netflix banners on PATCH /banners/{banner_id}. No operation, field, event, or error code was added or removed; only the descriptions of resource_type and resource_id changed.
Changes that may require action
resource_idnow takes the public ID of the resource, the same value an event returns inresource.id(les_30,op_01J8Z3...,rul_lesson_5). Before,les_30returned no events and only the internal number (30) matched.resource_idrequiresresource_type. Without it, or with a value that is not a public ID of that resource type (such as the bare number30), the request returns422 validation_failed. Send theresource.objectandresource.idof an event to find the other events about the same resource.
Fixes
PATCH /banners/{banner_id}keeps the visual settings of anetflixbanner (title, text, colors, background, video and call to action, configured in the dashboard). Before, any change toname,type,link,images,htmlorshowcasesreset them to empty values.
3.15.2
October 14, 2026
No operation, field, event, or error code was added, removed, or changed. The transition announced in 3.15.0 ends.
Changes that may require action
- The access token of
cademi auth loginsent together withX-API-Key(whatcademi-cli0.3.x sends) returns401 ambiguous_credentials, like any other OAuth access token together with a credential secret.cademi-cli0.4.0 and later send the access token alone; runcademi update.
3.15.1
October 14, 2026
Backward compatible with 3.15.0: no operation, field, event, or error code was added, removed, or changed. This release corrects descriptions that still described the combined mode retired in 3.15.0.
- The API description and
DELETE /administrators/{administrator_id}now describe the current model: an administrator's OAuth access token alone acts as that administrator, and a credential secret alone acts with the credential's policies. actor.admin_idofAuditEntryandRequestLogis the administrator who acted with their OAuth access token, and is null when a credential acted.Capabilitiesdescribes the capabilities of the caller, a credential or the administrator of an OAuth access token.- The
3.15.0entry below is corrected: the transition accepts the access token ofcademi auth loginsent together withX-API-Keybycademi-cli0.3.x, recognized by the token, not by the CLI version; and the audit trail shows the administrator inactor.admin_id, without the OAuth client that issued the token.
3.15.0
October 13, 2026
An administrator now uses the API as themself, with the OAuth access token alone and their own permissions; the combined mode (OAuth access token together with a credential secret) and the credential delegated to an MCP host are retired. This is not backward compatible for GET /credentials/current, the Credential object and POST /credentials; these changes are listed first. API v3 is still before its official launch, so they ship in a minor release.
Changes that may require action
- An OAuth access token issued to an administrator, by
cademi auth login(cademi-cli) or by the token exchange of the Cademí MCP (cademi-mcp), authenticates alone:Authorization: Bearer <access token>, withoutX-API-Key. The request acts as that administrator, with the administrator's own permissions (rootandmaster_adminhave all of them). Before, the token only worked together with the secret of a credential linked to the administrator, and the token exchanged by the MCP acted through a credential delegated to the host. - The CLI and the MCP authenticate only with OAuth for interactive use: the step that asked for a credential secret after
cademi auth loginis gone. During a transition, the access token ofcademi auth loginsent together withX-API-Key(whatcademi-cli0.3.x sends) is still accepted and acts as the administrator; theX-API-Keyheader is ignored. This transition ends in a later release, after the new CLI is available; send the access token alone. - Any other OAuth access token together with a credential secret (in
X-API-Keyor as the bearer) returns401 ambiguous_credentials. An access token whose administrator was removed, moved to another account or revoked returns401 invalid_access_token. - A credential is always autonomous:
auth_modeis no longer part ofCredential, andPOST /credentialswithauth_modereturns422 unknown_field. A credential that hadauth_mode: human_requirednow authenticates with its secret alone, inAuthorization: Beareror inX-API-Key. GET /credentials/currentno longer returnshuman. With the OAuth access token of an administrator,GET /credentials/currentandGET /credentials/current/policiesreturn404 not_found: no credential authenticated the request. UseGET /capabilitiesto read what the caller can do.- The error codes
human_context_required,human_mode_not_allowed,admin_not_eligibleandtenant_mismatchare no longer returned.
Administrators acting through the API
- The administrator's own permissions decide every operation, with the same refusals as a credential:
403 permission_deniedwithout the permission,404 not_foundoutside its scope (an administrator limited to some products does not see the others).GET /capabilitieslists them, and itslimits.ops_per_minuteis the per-caller limit. - Rate limit: requests of an administrator count in the account bucket, and in a per-administrator bucket with the account default for a credential; there is no per-administrator override.
delegation_limit_exceededapplies to administrators as callers too: a full-access administrator grants anything, includingadministrators.*andcredentials.*; any other administrator grants only what their own permissions grant, inPOST /credentials,POST /administratorsandPUT /administrators/{administrator_id}/permissions. Nobody changes their own permissions (409,reason: self_modification).- Idempotency keys, operations, configuration plans and event streams created by an administrator belong to that administrator, as they belong to a credential.
requested_by.kindof a processing attempt can beadministrator(with anadm_id), when an administrator requested it.- The audit trail shows the administrator in
actor.admin_id, with no credential.
If you use cademi auth login with a credential secret, update the CLI when the new version is available and drop the secret. If you read auth_mode or human from a credential, or send auth_mode when creating one, stop doing it.
3.14.0
October 12, 2026
Moves the permissions of administrators to the policy format that credentials already use, which is not backward compatible for GET and PUT /administrators/{administrator_id}/permissions and for POST /administrators; these changes are listed first. API v3 is still before its official launch, so they ship in a minor release.
Changes that may require action
GET /administrators/{administrator_id}/permissionsreturnsrole,full_accessandpoliciesinstead of thepermissionsmap andproduct_ids. Each item ofpoliciesis aPolicy, the same object asGET /credentials/{credential_id}/policies; access limited to some products is theidsselector ofproduct.rootandmaster_adminreturnfull_access: trueand no policies.PUT /administrators/{administrator_id}/permissionstakes{role, policies}. Each policy is{capabilities, resources}, as inPOST /credentials/{credential_id}/policies, and the policies replace the previous ones. Sendpolicies: []withrootormaster_admin; policies with them return422 validation_failed.permissionsandproduct_idsreturn422 unknown_field.revisionand theETagof both operations are the revision of the administrator's permissions. Send thatETaginIf-Match. TheETagofGET /administrators/{administrator_id}is not accepted there anymore and returns412 revision_mismatch.POST /administratorstakespoliciesinstead ofpermissions. Withoutpolicies, the administrator gets the default permissions of the role, limited toproduct_idswhen you send it.policiestogether withproduct_idsreturns422 validation_failed.delegation_limit_exceededmeans that nobody grants more than they have. A credential grants an administrator only the delegable permissions of its own policies, so neveradministrators.*orcredentials.*; before, it could grant anything in the default permissions of the chosen role. Nobody changes their own permissions: that returns409 state_conflictwithdetails[].reasonset toself_modification.published_byof aPolicycan be{"kind": "system", "id": null}: a revision the platform derived on its own, such as the previous permissions of an administrator converted to a policy.
An administrator can now hold only the permissions they need, such as access to API credentials without the account settings. GET /administrators/adm_42/permissions for a support administrator with only that access returns:
{
"data": {
"object": "administrator_permissions",
"role": "support",
"full_access": false,
"policies": [
{
"object": "policy",
"id": "pol_01JA2Z5K3N7Q8R9S0T1V2W3X4Y",
"capabilities": ["credentials.read", "credentials.manage"],
"resources": [{"type": "instance", "selector": "instance", "ids": [], "include_future": false}],
"version": 3,
"published_at": "2026-10-12T14:00:00Z",
"published_by": {"kind": "admin", "id": "adm_7"}
}
],
"revision": "1234"
}
}Permission catalog
billing.read,billing.manage_invoices,billing.manage_cards,activity.readandsettings.update_appearanceare administrator permissions of the dashboard (account billing, the activity log of users, and the appearance of the members area). They are not delegable, no operation requires them, and they are listed byGET /credentials/permission-catalogwithintroduced_inset to3.14.0.catalog_versionis3.14.0.
Credentials
- Administrators who are not
rootormaster_admincan only grant a credential the permissions they have themselves, when they create it or change its policies in the dashboard. Credentials created by them before keep their policies.
If you read or change administrator permissions through the API, move to policies and send the new ETag in If-Match. If a credential creates administrators with permissions, send policies within what the credential can delegate, or send only role and product_ids.
3.13.2
October 9, 2026
Fixes one response that did not match the document. The document does not change.
Reports
summary.by_statusofGET /reports/exportsis an object. On an account without exports it was returned as[]; it is now{}, as the document declares.
No action is required.
3.13.1
October 9, 2026
Documents behavior that the API already had and that the guides described without a source in the document. Nothing changes in requests or responses.
Declarative configuration
attributesof a manifest item says which fields come from the path of the REST operation:moduleandlessonrequireproduct_id, which accepts{"$ref": "<ref>"}. Alessonalso takesmodule_idandcontent. The other kinds take only the fields of the request body.errors[].codeof the validation report lists the codes of the manifest itself:required,cyclic_reference,reference_not_found,prohibitedandorder_set_mismatch. Errors inattributesuse the validation codes of the corresponding REST operation.warningsis reserved and currently always empty.
Certificates
- The
409ofPOST /certificateslists the values ofdetails[].reason:no_access,template_disabled,exam_not_passedandnot_completed.
Reports
summaryofGET /reports/exportsdeclarestotalandby_status, the number of exports per public status.
Event streams
- The limit of active event streams is documented: 5 per credential and 20 per account.
No action is required.
3.13.0
October 8, 2026
Fixes responses that did not match the document, failures of exports and import analyses, adds the release of the first import of an account, which can refuse a call that succeeded before, and changes the status code of three responses; these are listed first.
Changes that may require action
- The first import of an account is released by the Cademí team. Until the account has a processed import,
POST /imports/{import_id}/processing-attemptswithmode=initialreturns409 feature_disabledwithdetails[].reasonset tofirst_import_requires_release. Contact support to release it; later imports are processed normally. GET /webhooks/{webhook_id}returns404for a deleted webhook endpoint, as the list already did. It returned200with the full object before. If you read a webhook after deleting it, expect404.- A credential granted administrator permissions on specific administrators (an
idsselector) can update, delete, restore, change the permissions of and send access to those administrators. These operations returned403for them before. Administrators outside the selector and unknown IDs return404, as before. PATCH /sales/deliveries/{delivery_id}that archives a delivery already archived, or reactivates one already active, returns200with the delivery. Archiving an archived delivery returned404before.
Imports and exports
- Exports and import analyses no longer fail with "upload session not found". Every export generated through the API failed, and every analysis ended with zero rows and
failed. filters.product_idandfilters.tag_idofPOST /exportsaccept a single ID or a list of IDs, as the response already returned them. A list was refused with422 unknown_field.GET /exports,GET /exports/{export_id}andGET /reports/exportsno longer list records without any date, which were never exports and returnedcreated_atasnull.
Responses that now match the document
operations.by_statusandrequests.by_outcomeofGET /account/usage,diffof audit entries and request logs, andvaluesofGET /users/{user_id}/custom-fieldsare objects. An empty value was returned as[].typeof a release rule anddenial_reasonof product access listnullin their enum, as they were already nullable.reissueof a certificate is described as what it is: whether the user already had a certificate for the product when this one was issued, including revoked or deleted certificates. To know which certificate a reissue replaced, usesupersedes_certificate_id.
Other fixes
DELETE /automations/diamond/{diamond_id}on a Diamond with memberships returnsdetails[].reasonset tohas_memberships.GET /gamification/scoreswithout filters no longer times out on accounts with few entries.- Correcting the reply of a reopened question with
PATCH .../questions/{question_id}/replies/{reply_id}moves the question back to answered.
If you process imports, expect first_import_requires_release on the first import of a new account. If you read webhooks after deleting them, handle 404.
3.12.2
October 7, 2026
Documents fields and responses that the API already returned, so that a client generated from the spec reads every field it receives. One response changes shape in the document and is listed first. The only change in behavior is that a ticket without a user returns user as null instead of omitting it.
Changes that may require action
POST /event-streamsdeclares its429as eitherRateLimitErroror the standardError. Reaching the limit of active event streams (too_many_streams) always returned the standard error envelope without the rate-limit fields; the document now says so. If your client reads every429of this operation asRateLimitError, checkerror.codefirst.
Documented fields
UserAttributionlists its fields:utm_source,utm_medium,utm_campaign,utm_content,utm_term,page_name,adcampaign,groupid,adid,gclid,fbclid,cargo,nome_empresaandtamanho_empresa, each a string ornull.- Items of
GET /support/comments,GET /support/questionsandGET /enrollmentsdeclarelinks.self, the canonical path of the item. GET /users/{user_id}/custom-fieldsdeclaresdata.object(user_custom_fields).ValidationFaileddeclaressupported_kinds, returned withunsupported_resource.- Every operation declares
403for refusals of the authenticated principal in human mode (admin_not_eligible,tenant_mismatch), under the newForbiddenresponse. - Every operation declares
503(Unavailable) for when the credential, its policy, or the rate limit cannot be verified. Only file, upload and import operations declared it before.
Support
userof a ticket isnullwhen the ticket has no user, as the schema already declared. It was omitted before.
3.12.1
October 6, 2026
Corrects the document so that it describes what the API already returned, and changes the format of two fields of score adjustments, which is not backward compatible; it is listed first.
Changes that may require action
- In
score_adjustedandscore_deletedprogress adjustments,result.score_idandresult.product_idare public IDs (sco_,prd_) instead of internal numbers. Read them as strings:score_idis theidofGET /users/{user_id}/scores/{score_id}, andproduct_idisnullwhen the entry is not tied to a product. These two fields were not in the3.12.0document, so the release comparison did not report the change.
Progress adjustments
ProgressAdjustment.typelists every value the API returns:reset,exam_result_deleted,exam_attempt_reopened,score_adjustedandscore_deleted. The enum is extensible, so new types may appear.resultdocuments the keys of each type.
Gamification
- The example of the
score.deletedwebhook shows a manual deletion, withexam_idset tonull.exam_idandadjustment_idare never filled together. - The description of
Score.originsays that a balance is also corrected by deleting a manual entry withDELETE /users/{user_id}/scores/{score_id}. GET /credentials/permission-catalogreportsintroduced_inas3.12.0forscores.delete, the release that added it.catalog_version, in that response and inGET /capabilities, moves from3.2.0to3.12.0with it, and policies published from now on record3.12.0.
Support
- The description of
CommentListno longer mentionsinclude_replies, which is not a parameter ofGET /support/comments. Replies from users are always listed.
If you read result.score_id or result.product_id of score adjustments, read them as public IDs.
3.12.0
October 6, 2026
Adds the deletion of manual point entries and replaces the score.removed event with score.deleted, which is not backward compatible for webhook destinations and event streams that subscribe to score.removed; it is listed first.
Changes that may require action
score.removedis replaced byscore.deleted. Deleting a resource is.deletedin every other event, andscore.removedwas the only exception.score.deletedis published once per deleted point entry, both when the points of a passed exam are reversed (exam result deleted or attempt reopened) and when a manual entry is deleted.datacarriesidof the entry,user_id,product_id,points,exam_id(nullfor a manual entry) andadjustment_id(nullfor an exam reversal). Webhook destinations and event streams subscribed toscore.removedare moved toscore.deletedby the platform; events already delivered keep their original type.
Gamification
DELETE /users/{user_id}/scores/{score_id}deletes a manual point entry, such as one created byPOST /users/{user_id}/score-adjustmentsor added in the dashboard. It requiresreason, records the deletion as a progress adjustment, changes the balance by the negative of the entry's points and needs the newscores.deletepermission. Points earned automatically return422 score_not_removable.- Points added in the dashboard are now score adjustments, the same as
POST /users/{user_id}/score-adjustments: they publishscore.adjustedand are recorded as progress adjustments with their author and reason.
Support and sandbox
GET /support/commentsdeclares its items asoneOfCommentorCommentReply, told apart byobject. Replies were already listed since3.11.0; only the document changed, so a client generated from the spec can read them.- The description of
POST /sandbox/resetspoints toPOST /sandbox/test-scenarios/{scenario_id}/runs.
If you subscribe to score.removed or handle it in code, switch to score.deleted and read id to identify the entry. To delete manual entries, grant scores.delete to the credential.
3.11.0
October 6, 2026
Backward compatible with 3.10.2: fields, parameters and values were added, and no operation, field, event, or error code was removed or changed. This release completes the support and delivery contract.
Support
- Departments carry
admin_id, the responsible administrator (nullwhen none).POST /support/departmentsandPATCH /support/departments/{department_id}accept it, andPATCHwithnullclears it. Renaming a department no longer clears the administrator. DELETE /support/departments/{department_id}acceptsunlink_tickets=trueto unlink the department's tickets, including deleted ones, and delete it, as the dashboard does. Without it, a department with tickets is still refused withstate_conflict.- Tickets carry
queue:openawaits a staff reply,answeredawaits the user,closedis closed.GET /support/ticketsfilters byqueue, alongsidestatus. POST /support/tickets/{ticket_id}/repliesaccepts a reply with only attachments:textcan be omitted whenfile_idsis present.GET /support/commentsalso lists pending replies from students, and comment replies carrycomment_id, the top-level comment. A student reply can be deleted individually withDELETE /products/{product_id}/comments/{comment_id}/replies/{reply_id}.
Deliveries and access
- Release rules represent the closing date and the unit:
paramscan carrycloses_after_daysandunitfor relative rules andcloses_onfor date rules, andexam_idis the public exam ID (exm_). PUT /sales/deliveries/{delivery_id}/productsandPUT /sales/deliveries/{delivery_id}/rulesdescribe that a delivery is live: changes apply to existing enrollments, except users with an individual override.PATCH /users/{user_id}/products/{product_id}/accessacceptsduration: nullto remove the individual duration and inherit the delivery's again.
If you read params of release rules, handle the new optional keys. To delete a department with tickets in one call, send unlink_tickets=true. No other action is required.
3.10.2
October 6, 2026
Backward compatible with 3.10.1: no operation, field, event, or error code was added or removed. This release corrects descriptions and one request schema, and deprecates admin_two_factor_available.
Permissions
x-cademi-permissionsentries can carryany_of. Entries with the sameany_ofvalue are alternatives: any one of them grants access. Entries withoutany_ofare all required.GET /reports/rankingsis the first operation to use it:reports.readorrankings.read.
Deliveries
- The request body of
POST /sales/deliveriesdocumentsproductswith its own input schema,entries, the same shape as the body ofPUT /sales/deliveries/{delivery_id}/products. It previously pointed to the response schema, which listedobjectand the read-onlyignored_external_product_idsas required. Both are now optional and ignored when sent. Requests accepted before are still accepted. PATCH /sales/deliveries/{delivery_id}explains thatdeletedis an alias ofstatusand also requiresdeliveries.archive.
Settings
- Two-step verification is now available to every administrator on every account, with no per-account switch.
admin_two_factor_availableinGET /settings/authenticationis deprecated and alwaystrue;PATCH /settings/authenticationstill accepts it and ignores it. The field stays until v4.
Sandbox and support
POST /sandbox/resetsandPOST /sandbox/test-scenarios/{scenario_id}/runsexplain that403 sandbox_onlyand409 sandbox_requiredboth mean the request must be made with a sandbox credential.DELETE /support/departments/{department_id}explains how to clear a department before deleting it, including when only deleted tickets remain.
If you set admin_two_factor_available, you can stop sending it: it has no effect. No other action is required.
3.10.1
October 6, 2026
Backward compatible with 3.10.0: no operation, field, event, or error code was added, removed, or changed. This release documents behaviors already in place.
Lists and reorders
limitdeclares its default of50inGET /users,GET /banners,GET /showcases,GET /showcases/{showcase_id}/products,GET /products/{product_id}/lessonsandGET /products/{product_id}/exams/{exam_id}/questions.- The reorder operations of banners, showcases, products of a showcase, product content, module content and exam questions declare that
idsneeds at least one item, and their descriptions explain how to obtain the complete set to send. In module content, the set covers both direct submodules and direct lessons. GET /support/tickets/{ticket_id}/repliesstates that replies are ordered bycreated_atand then ID, with no newest-first option.
Users
GET /usersdescribes howaccessanddeletedcombine:access=nonemeans users in the trash, and it does not test product enrollments.POST /usersdocumentscustom_fieldsas a map of custom field IDs to values, andPATCH /users/{user_id}documentssettingsand its three restriction flags, including that sending the object replaces all three.
Files and usage
GET /account/usageexplains thatstorage.used_bytescomes from a stored total, updated on each upload and deletion through the API and recalculated daily.- The storage quota is described as 50 GiB, and the attachment limit in
POST /uploadsas set by the account.
Webhook deliveries
- A
400answered withQueue is full.(the response Make sends when the queue of a webhook is full) no longer closes the delivery asfailed. The delivery waits without spending attempts, 1 minute after the first request and doubling up to 30 minutes, until the queue accepts it or the delivery deadline passes. It does not count toward disabling the webhook, and the audit trail recordswebhook.reschedulewith reasondestination_queue_full. Any other400is still a final rejection.
No action is required.
3.10.0
October 3, 2026
Backward compatible with 3.9.1: no operation, field, event, or error code was removed or changed. This release paginates the content of products and modules and the replies of tickets by cursor, returns whole collections from reorder operations, and documents request bodies and behaviors already in place.
Pagination
GET /products/{product_id}/contentandGET /products/{product_id}/modules/{module_id}/contentacceptlimit(1 to 200, default 200) andcursor, and returnnext_cursorwhen more content follows. Content with up to 200 entries still comes in a single page; before, entries beyond the 200th were dropped without a cursor. An invalid cursor fails with400and theinvalid_cursorerror code.GET /support/tickets/{ticket_id}/repliesnow documentslimit(1 to 200, default 50),cursorand the400withinvalid_cursor, which it already accepted. The cursor follows the order of the list, creation date and then ID, so pages no longer skip or repeat replies whose dates are out of the order of their IDs. Cursors issued before this release are still accepted.- The reorder operations of product content, module content, banners, showcases, products of a showcase and exam questions return the whole reordered collection. Before, the response stopped at 200 entries.
- The descriptions of comment replies, import processing attempts, Diamond triggers and the report catalog now state that they return the whole collection in a single response.
Request bodies
- The request bodies of the settings updates, banner and menu item updates, email template previews and test messages, user attribution, and the creation of products, custom code blocks and custom fields are documented field by field, in named schemas such as
UpdateSecurityInput. Requests accepted before are still accepted. - Update and delete operations that already honored the
If-Matchheader now declare it as a parameter: administrators, imports, taxonomies and their terms, email bounces, custom fields, tags, users and product access of users. PATCH /settings/custom-code/{custom_code_id}carriesx-cademi-manual-checks, an informative extension: changingnameorcontentof a block whosetypeispushalertcorequires thecustom_code.manage_pwapermission. The description of custom code creation now explains that a new block is active immediately and where it is rendered.
Files
- Export files no longer expire: they are kept until the export is deleted, and
expires_atisnull. Only exports withstatusexpired, generated while files were retained for 7 days, return the end of that period. attachments[].urlin ticket replies is a temporary link that expires up to 15 minutes after the response. To get a fresh one, request the reply again or create a link withPOST /files/{file_id}/download-links.
If you store attachments[].url of ticket replies, store the file ID instead and request a new link when needed. To read more than 200 entries of the content of a product or module, follow next_cursor. No other action is required.
3.9.1
October 1, 2026
Backward compatible with 3.9.0: no operation, field, event, or error code was added, removed, or changed. This release documents two behaviors.
Webhooks
enrollment.updatedis now also sent when an enrollment is suspended or reactivated by a sales platform event (cancellation, refund, chargeback, subscription renewal), within about a minute. Scheduled cancellations are reported when they take effect. The payload did not change.- The description of
testin the webhook envelopes now states that a test delivery uses the detail level the endpoint receives at that moment,health.effective_payload_detail, which can be lower than the configuredpayload_detail.
No action is required.
3.9.0
September 30, 2026
Backward compatible with 3.8.0: no operation, field, event, or error code was removed or changed. This release adds filters per event type to webhook endpoints, marks test deliveries, and fixes resource.id in the events API.
Filters per event type of webhook endpoints
- Webhook endpoints have
event_filters: per subscribed event type, filters on fields of the event (product, lesson, exam, legal term, delivery, custom field, added tag, passed, minimum progress, milestones, signup origin, score origin, filled). Writable inPOST /webhooks,PATCH /webhooks/{webhook_id}and thewebhookkind of configuration manifests; returned byGET /webhooksandGET /webhooks/{webhook_id}, with IDs as public IDs. - An event is delivered only when it passes both
resource_filtersand the filters of its event type. Existing endpoints returnevent_filters: nulland receive the same events as before. - Invalid filters fail with
422 validation_failed, anddetails[].fieldpoints to the event type or filter, such asevent_filters.exam_attempt.completed.passed. - A
PATCHthat changesevent_typeswithoutevent_filtersdrops the filters of the event types removed.
Test deliveries
- A test delivery sent with "Send test" on the webhook endpoint page of the dashboard carries
test: trueat the end of the envelope. It is signed like a real delivery, with the same headers, but holds sample data, itsidanddelivery_iddo not exist in the API, and it is not retried. Real deliveries do not include the key.
Fixes
resource.idinGET /events,GET /events/{event_id}and event streams is now always the public ID of the resource, with the prefix of its type. Events whose payload has noidreturned the internal number instead:lesson_progress.completedreturned"30"and now returnsles_30. This affectscertificate_template.updated,comment.reply_created,diamond_membership.reprocess_requested,diamond_membership.run_failed,diamond_membership.stage_changed,import.process_requested,lesson_progress.completed,product_progress.completed,product_progress.milestone_reached,product_progress.started,product_progress.updated,progress.adjusted,question.reply_published,release_rule.updated(now therule_idof the payload),sales_event.reprocess_requested,score.removed,ticket.reply_createdanduser.access_updated. Webhook bodies have noresourceobject and did not change.
No action is required. To filter what an endpoint receives per event type, set event_filters. If you read resource.id of the events API as a number, read it as the public ID.
3.8.0
September 30, 2026
Backward compatible with 3.7.0: no operation, field, event, or error code was removed or changed. This release adds four events about what students do, and user.tags_updated is now sent whenever the tags of a user change, and only then.
New events
Four public events. They are read with events.read, can be subscribed to in event_types of POST /webhooks, and carry IDs only, like every event.
comment.created: a user posted a top-level comment on a lesson, including a comment that is waiting for moderation. Resourcecomment. Payload:id,user_id,product_id,lesson_id. A reply of a user to a comment is not sent, and neither are comments posted by an administrator or through the API.question.created: a user asked a question on a lesson. Resourcequestion. Payload:id,user_id,product_id,lesson_id.user.custom_field_updated: the value of a custom field of a user changed: in the student area, in the admin panel, inPOST /users,PUT /users/{user_id}/custom-fieldsorDELETE /users/{user_id}/custom-fields/{custom_field_id}. One event per field whose value changed; saving the value the field already has sends nothing. Resourceuser. Payload:id(the user),custom_field_idandfilled(falsewhen the value was cleared). The value is never sent: read it withGET /users/{user_id}/custom-fields.user.first_accessed: the first access of a user to the student area. Sent once per user. Resourceuser. Payload:id.
Changes to user.tags_updated
- It is now also sent when tags are applied by a delivery (a manual enrollment, "apply tags to all users" and
POST /sales/deliveries/{delivery_id}/tags/applications), by a sale or by an import, and when tags are added through the legacy API v1. These arrive within a few minutes of the change, withtag_idsas they are when the event is created. - It is no longer sent when nothing changed:
POST /users/{user_id}/tags/{tag_id}with a tag the user already has, andPUT /users/{user_id}/tagswith the tags the user already has, publish no event. - The format did not change, and its version is still
1:tag_idslists every tag of the user after the change.
No action is required. To receive the new events, add them to event_types of a webhook endpoint.
3.7.0
September 29, 2026
Backward compatible with 3.6.2: no operation, field, event, or error code was removed or changed. This release adds a payload detail level to webhook endpoints and publishes a typed schema for every public event.
Payload detail of webhook endpoints
payload_detailis accepted byPOST /webhooksandPATCH /webhooks/{webhook_id}and returned byGET /webhooksandGET /webhooks/{webhook_id}:ids(the default),maskedorfull. Existing endpoints areidsand receive the same body as before.idssends onlydata, which identifies people by ID. Its only free text is the note written by whoever operates the account (reasonincertificate.revokedanddiamond_membership.stage_changed), sent as written.maskedandfulladd anexpandedobject next todata, with the resources the event refers to (such asuser,productandlesson) in the same format as the API returns them, as they were when the delivery was created. A key isnullwhen the resource no longer exists, and event types that refer to no expandable resource send{}. Products, lessons and exams have the format of the listing operations: for a product,displayanddefinitionsare empty objects andofferis not included.- In
masked, personal data is masked by fixed rules, now described inpayload_detail(re**@g**.com,Renan C** P**,(13) *****8923), and free text written by users is replaced with[redacted]. Masked personal data is still personal data, and a masked value may not match theformatof its field. Infull, values are sent as stored. - Setting
full, or changing the event types of afullendpoint, requires theusers.read_personalpermission, plustickets.read_personalwhen an event type expands aticketandlegal_terms.read_proofwhen one expands aterm_acceptance. Without them, the request fails with403and the new error codepersonal_data_permission_required, whichconfiguration.applycan also return per item (OperationError). - When the credentials or the administrator that created a
fullendpoint lose one of those permissions, new deliveries are sent asmaskeduntil the permission is restored.GET /webhooks/{webhook_id}returns the level in use inhealth.effective_payload_detail. - A delivery keeps the
expandedobject built when it was created. After the level of an endpoint is lowered, a retry of an older delivery is sent withoutexpanded.GET /webhooks/{webhook_id}/deliveriesandGET /webhooks/{webhook_id}/deliveries/{webhook_delivery_id}return the level at which each delivery was created inpayload_detail(idswhen it has noexpanded). - Every change of level is recorded in
GET /audit-entriesaswebhook.payload_detail_changed, with the previous and the new level.
Typed event schemas
- The OpenAPI document has a
webhookssection with one entry per public event type, and one schema per type incomponents.schemas, such asLessonProgressCompletedEvent. Each schema describes the envelope, every key ofdata(type, description, example, whether it can benulland the prefix of each ID) andexpanded, which references the schemas of the objects of the API. The event catalog still lists the payload keys of each type.
Other changes
- In
diamond_membership.run_failed,erroris now a code,stage_run_failed, instead of the technical message of the failure, which could contain internal details. The format of the event did not change, and its version is still1. Events recorded before this change keep the original text until they expire. validation_urlof a certificate always points to the default address of the account, which redirects to its custom domain when there is one. It pointed to the host of the request.- The descriptions of
reasoninPATCH /users/{user_id}/certificates/{certificate_id}andPATCH /automations/diamond/{diamond_id}/memberships/{membership_id}now say that the text is sent as written in the event, at every payload detail level.
No action is required. To receive expanded, set payload_detail on the endpoint.
3.6.2
September 29, 2026
Backward compatible with 3.6.1: only a description changed.
- The description of
resource_filtersinPOST /webhooksandPATCH /webhooks/{webhook_id}said that each key is theresource.typeof the event. Webhook deliveries do not carry the resource of the event, and inGET /eventsthe field isresource.object. Each key is the resource type of the event type, as listed in the event catalog. The behavior did not change.
No action is required.
3.6.1
September 29, 2026
Backward compatible with 3.6.0: no operation, field, or error code was removed or changed.
resource_filtersinPOST /webhooksandPATCH /webhooks/{webhook_id}accepts public IDs (prd_7) in addition to numeric IDs (7). IDs are stored and returned in numeric form. An ID that does not belong to the resource type of its key returns422 validation_failedon that item, as a non-numeric value already did. The schema now declares the shape: an object keyed by resource type, each key with a list of IDs.- A resource filter applies only to events whose own resource is of that type.
lesson_progress.completedis about alesson, so aproductfilter does not restrict it; list the lessons underlessoninstead. Events whose resource type is not a key ofresource_filtersare delivered without restriction. This was already the behavior in3.6.0. PATCH /webhooks/{webhook_id}with anhttp://localhostURL in a sandbox account returns422 validation_failedonurl, likePOST /webhooks. It returned500. The description ofPOST /webhooksno longer says thathttp://localhostis accepted in sandbox accounts: webhook URLs use HTTPS in every account.- Webhook deliveries and their attempts are retained for at least 60 days after the delivery is created, and a delivery that has not reached a final status is never removed. The descriptions of the delivery and attempt listings now say so.
No action is required.
3.6.0
September 28, 2026
Backward compatible with 3.5.0: no operation, parameter, field, or error code was removed or changed. This release adds five events and changes how webhooks are delivered. One change may require action: the IDs of the Diamond triggers.
Changes that may require action
- Diamond trigger IDs change. When this release is deployed, every Diamond trigger gets a new ID (
trg_...), in every account: inGET /automations/diamond/{diamond_id}/triggersandGET /automations/diamond/{diamond_id}/triggers/{trigger_id}, intrigger_idofGET /automations/diamond/{diamond_id}/stages, and in thediamond_trigger.updatedevent. The trigger of each stage, its settings and what it delivers to your automation do not change. If you stored trigger IDs, list the triggers of each Diamond again withGET /automations/diamond/{diamond_id}/triggersand match them bystage_id. An old ID returns404, and anIf-Matchtaken before the deploy returns412.
Other changes
New events: learning progress. Five public events report the progress of a user in a product. They are read with events.read, like progress.adjusted, and can be subscribed to in event_types of POST /webhooks.
lesson_progress.completed: a user completed a lesson. Resourcelesson. Payload:user_id,product_id,lesson_id.product_progress.started: a user completed the first lesson of a product. Resourceproduct. Payload:user_id,product_id.product_progress.completed: the progress of a user in a product reached 100%. Resourceproduct. Payload:user_id,product_id.product_progress.milestone_reached: the progress crossed one or more milestones (50, 75, 90) with a single completed lesson. Resourceproduct. Payload:user_id,product_id,milestones(for example[50, 75]).product_progress.updated: the progress of a user in a product after each completed lesson. Resourceproduct. Payload:user_id,product_id,percent(integer from 0 to 100, the same value aspercentinGET /users/{user_id}/products/{product_id}/progress).
resource_filters with product or lesson restricts them to the listed products or lessons. product_progress.updated is published for every completed lesson, so subscribe to it only if you need each change. Completing a lesson that was already completed publishes nothing, and resetting progress is still progress.adjusted.
Webhook delivery changes. A 429 from your endpoint no longer counts as an attempt: the delivery waits for Retry-After (seconds or an HTTP date, up to 6 hours; 60 seconds when absent) and keeps its attempts. Deliveries to a webhook URL that failed three times in a row (5xx, timeout or connection error) wait without spending attempts; one test delivery goes out every 5 minutes until the URL responds. Each URL has its own circuit, so other URLs on the same host keep receiving; the rate limit learned from a 429 still applies to the whole host. timeout_ms of the webhook now applies (1 to 10 seconds). A paused webhook keeps its deliveries pending, and a disabled one cancels them before sending. GET /webhooks/{webhook_id} gains health.circuit_state (closed, open, half_open). The audit trail gains webhook.reschedule, webhook.circuit_opened, webhook.circuit_closed and webhook.rate_limit_learned. The attempt field of the envelope counts every request, including those answered with 429.
Automatic deactivation of failing webhook endpoints. An endpoint with 10 different deliveries failing in 24 hours (server error, connection failure or timeout; 4xx, 429 and rescheduling do not count), or whose URL failed 30 times in a row, is deactivated by the platform (only the endpoints with that URL; other URLs on the same host are not affected): it shows as inactive, its pending deliveries are canceled, and the administrators of the account are notified by e-mail. PATCH /webhooks/{webhook_id} with status: active reactivates it and sends back to the queue the deliveries from the last 48 hours that ended as dead or were canceled by the deactivation; status: inactive on such an endpoint does not change it. The audit trail gains webhook.dead, webhook.disabled and webhook.reactivated.
Sandbox accounts now receive every event they generate. Events caused from the admin panel, the student area or background jobs of a sandbox account were recorded as production and did not reach the sandbox webhooks, GET /events or event streams; they are now recorded with environment: sandbox and delivered to the webhooks registered in the sandbox. Legacy webhooks configured in a sandbox account are now delivered too. Production accounts are not affected.
Platform metrics no longer count sandbox accounts. The daily aggregates behind the reports are no longer computed for sandbox accounts, and what had already been computed for them was removed. In a sandbox account, GET /reports/users returns an empty series (the summary is unchanged) and the aggregated columns of GET /reports/products and GET /reports/lessons (views, completion) return zero. Production accounts are not affected.
Apart from the Diamond trigger IDs, no action is required.
3.5.0
September 28, 2026
Behavior change in POST /uploads and POST /uploads/{upload_id}/completions: uploads are now checked against a fixed list of extensions and a maximum size for each purpose.
image:.jpg,.jpeg,.png,.gif,.webp, up to 10 MiB.pdf:.pdf, up to 100 MiB.import:.xls,.xlsx, up to 20 MiB.editoranddocument: documents (.pdf,.doc,.docx,.xls,.xlsx,.xlsm,.ppt,.pptx,.pps,.ppsx,.odt,.ods,.odp,.txt,.csv,.rtf,.md,.epub), images (.jpg,.jpeg,.png,.gif,.webp,.svg), audio (.mp3,.m4a,.wav,.ogg,.aac,.opus) and archives (.zip,.rar,.7z), up to 100 MiB, or less when the school sets a lower attachment limit.- Video is not accepted for any
purpose. Host videos through the integrated video players. - A
filenamewithout an extension or with an extension outside the list returns422 validation_failedonfilename(codeextensions); asize_bytesabove the limit returns422 validation_failedonsize_bytes(codemax). The messages name the accepted extensions and the limit. No upload session is opened. - On completion, a file whose actual size exceeds the limit is removed, the session becomes
failedwithfailure_reasontoo_large, and the API returns422 validation_failedonsize_bytes. A session whosefilenameextension is outside the list of itspurposefails the same way, withfailure_reasonextension_not_allowedand422 validation_failedonfilename.
Action required only if you upload videos or other files outside these lists: they were previously accepted up to 5 GB and are now rejected.
Files uploaded through the API now expire when unused. A file uploaded through an upload session must be used by a resource within 24 hours of the upload being completed; otherwise it is deleted from storage. The File object gains expires_at: the deletion deadline (completed_at + 24 hours) while the file is still unused, and null once a resource uses it or for files uploaded outside the API, which never expire under this rule. A file counts as used when it is set as a lesson attachment, an import source, a ticket or ticket reply attachment, a user avatar, an image of a product, module, lesson, taxonomy term, banner or exam question, a settings asset (branding, admin area, app, access screens, emails, sharing), a certificate template background, or when its path or public URL appears in a block (data.file.url or data.url) of lesson content, an FAQ answer, a legal term or a question reply. Once used, the file never returns to pending, even if the resource is later removed. An expired file is no longer listed by GET /files, GET /files/{file_id} returns 404 not_found, and it can no longer be referenced by file_id.
Action required if you upload files and reference them later: use each file in a resource within 24 hours of completing the upload.
Storage quota per account. Each account now has a storage quota, 50 GB unless a different quota was agreed, which also covers the accounts billed with it, such as its replicas. Files attached to student questions do not count toward it. POST /uploads rejects an upload whose size_bytes would take the storage in use above the quota with the new error code 409 quota_exceeded, before any session is opened; details[0] carries reason set to storage_quota, used_bytes, and quota_bytes. POST /uploads/{upload_id}/completions checks the actual size again: a file above the quota is removed, the session becomes failed with failure_reason storage_quota, and the API returns the same 409 quota_exceeded. GET /account/usage gains the required storage object with used_bytes and quota_bytes, independent of period. Files are never deleted or hidden because of the quota.
3.4.7
September 25, 2026
Backward compatible with 3.4.6.
If-Matchon lesson attachments (update and delete) is checked atomically with the write: of two concurrent writes with the sameETag, one succeeds and the other returns412 revision_mismatch.DELETEof an attachment now acceptsIf-Match.- The revision of a lesson changes on every write, even when two writes happen within the same second.
- The message for a malformed ID no longer names the resource type, and an email in use has the same message on create and on update.
No action is required.
3.4.6
September 25, 2026
Backward compatible with 3.4.5.
- Writes to the body of a lesson (attachments and content) no longer overwrite each other: an attachment added while the lesson content is being updated is kept.
- Reprocessing a Diamond membership that has no failed run returns
409 state_conflictwith reasonno_failed_run. - A malformed public ID in the request body returns
422with codeinvalidand a message that names the expected resource. - A failed item of an asynchronous operation always has
codeandmessage, and afailedoperation always haserror(the first failed item, oritems_failed). 409 already_existsfor an email in use points tofield: email.- An unreadable import file returns the documented message instead of the parser error.
422 idempotency_key_reusedtells a key reused for a different resource (reasondifferent_resource) from a key reused with a different body (reasondifferent_body).
No action is required.
3.4.5
September 25, 2026
Backward compatible with 3.4.4. Every error now carries a specific code and a translated message:
- Requests that returned
500with valid input now return the documented error: public IDs above the supported range (404, or422when the ID is in the body), invalid dates in report windows, a module group with a parent, emails rejected by the email rules, values longer than the stored field, a list where text is expected, and anexternal_idalready in use (409 already_existsonexternal_id). Legacy point records no longer break score listings. - An ID in the request body that cannot be parsed is
422on its field. It was stored as an invalid reference in some module and lesson moves. - Changes to replicated content are rejected with
403 replica_readonlyconsistently. - Domain rejections have their own codes and reasons: a user without access to the product (
409 state_conflict, reasonno_access), a revoked enrollment (reasonrevoked), a certificate or exam result that no longer exists (404), and areasonin every409 state_conflictthat has more than one cause. - Every item in
detailshas acodeand amessagein the language of the request, anddomain_rule_violationexplains the rule. - Asynchronous operation items report failures with
code,message, andfieldorreason, described by the newOperationErrorschema, instead of a genericnot_found. - Reusing an
Idempotency-Keyfor a different resource or a different body returns422 idempotency_key_reusedin every state. A malformedIf-Matchreturns400 if_match_invalid;If-Match: *matches any existing revision. A JSON body that is not an object returns400 malformed_json. - The document declares the common error responses (
406,409idempotency conflicts,400,413, and415where they apply) and thepageobject of every paginated list.
No action is required.
3.4.4
September 25, 2026
Backward compatible with 3.4.3. Fixes found by an end-to-end sweep of all operations against a sandbox:
- Operations that failed with
500now succeed or return the documented error: creating a user with an@sandbox.invalidemail (and an email with an unknown suffix is422onemail), deleting a support ticket reply, deleting a revoked certificate, and creating an import when the file storage is unavailable (503 storage_unavailable). PATCH /support/tickets/{ticket_id}works on tickets whose user was deleted, and applies all fields in one transaction.PATCH /settings/securityonly enforces provider credentials for the providers the request changes.- Resetting progress of a user who never opened the product returns
409 progress_not_started. - Imports report unreadable files, empty files, and missing columns with their own codes (
import_file_unreadable,import_file_empty,import_column_missing). - Every
4xxerror has its own message, and every422 validation_failedlists itsdetails. - Webhook delivery attempts expose
id; the certificate template update declares its own request body; custom code declares the keys ofcontent;purpose=importuploads accept only.xlsx. - The sandbox test data now include two user tags and one custom field, and a sandbox reset no longer fails after webhooks were delivered.
No action is required.
3.4.3
September 25, 2026
Backward compatible with 3.4.2: no operation or error code changed.
- Each item in
detailsof422 unknown_fieldand400 unknown_parameternow carriescodeandmessagein addition tofieldorparameter, as the error guide describes. - Validation messages in
details[].messageare translated, in the language of the response (Accept-Language). They were returned as raw keys such asvalidation.in. POST /event-streamsacceptsstart:earliest(the default, the previous behavior) delivers the retained events from the beginning;nowdelivers only events that occur after the stream is created.
No action is required.
3.4.2
September 25, 2026
Backward compatible with 3.4.1: no operation or error code changed. The OpenAPI document is complete enough to generate typed clients:
- Every success response other than
204declares its body. Support comments, questions, tickets, departments, and FAQs now reference their schemas, with new list schemas for the collections. event_streams.events.getdeclares its200astext/event-stream, marked withx-cademi-stream: sse.- Every path and query parameter has a description.
- Properties that were declared without a type (
Product.display,Product.definitions,Product.offer,Module.settings,EventStream.filters,Lesson.settings,TestScenario.params) are typed as objects. rankings.listandreports.rankings.listacceptproduct_id[], like the other array filters.product_idis kept as a deprecated alias and now also accepts a single value.EventStream.filtersis returned as{}when the stream has no filters, as documented. It was returned as[].
No action is required.
3.4.1
September 25, 2026
Backward compatible with 3.4.0: no operation, field, or error code changed. The OpenAPI document is now valid for strict validators and client generators:
- The
paramsfields of release rules and of the release calendar are declared as objects. They were serialized as[], which is not a valid schema. - The
scopesof the OAuth flow is serialized as an empty object instead of an empty array.
No action is required. If a code generator rejected the 3.4.0 document, use this one.
3.4.0
September 25, 2026
This release fixes behavior that did not match the documentation. A few of the fixes change the contract in ways that are not backward compatible; they are listed first, because the API is not yet in general use and correcting them now avoids carrying them into every future release.
Changes that may require action
- Credential scope is now enforced everywhere it was documented. A credential restricted to some users only reaches those users under
/users/{user_id}/...;enrollments.list,users.products.access.*, and the certificate operations only return or change data for products in scope. Out of scope is404, identical to a resource that does not exist. A reference in the request body that is out of scope (delivery_idinusers.enrollments.create,product_idintickets.create) is422on that field. deletedinPATCHaccepts onlyfalse(restore).truewas accepted and ignored; it is now422. Use theDELETEoperation to delete.showcases.createplaces the new showcase afterafter_id, as the name says. It was placed before it. Anafter_idthat does not exist is now422.administrators.updateno longer acceptsstatus, which had no effect.files.listaccepts onlysort=-created_at, the order it always used.uploads.createaccepts only the upload purposesimage,pdf,import,editor, anddocument.exportis reserved for files the platform generates.sandbox.resets.createcalled with production credentials returns403 sandbox_only. Theproduction_onlycode is removed.- In declarative configuration, lessons use
name, like the REST body.titleis now an unknown field. Idempotency-Keyis now declared on every operation that requires it. The server already required it; only the document changed.
Fixes
- Lists paginate by cursor where they did not: user term acceptances, tags (no longer capped at 200), banners, and email bounces. Comments honor
sort. Scope is applied before the page is cut, so a page is never short with a missingnext_cursor. positiontakes effect on create (products, modules, lessons) and on update (products, modules, lessons, showcases), counting from 1; a value past the end places the item last. Moving an item to another parent withoutpositionplaces it last.users.listappliesstatus,access,created_after, andcreated_before.files.download_links.createappliesinlineanddownload_name.exports.createwithuser_activityrequires a validuser_id(422), instead of producing an empty file.- Declarative configuration applies all nine resource kinds, including banners, menu items, settings, and custom code; reapplying the same manifest converges without duplicates; a plan applies only once (
409 state_conflict, reasonplan_already_applied); a masked secret sent back is422(reasonmasked_secret);order_set_mismatchis reported by validation with200andvalid: false. - Settings and email templates have their own revision: a change to one group no longer invalidates the
ETagof another. credentials.updateacceptssuspended, the status the API returns;inactiveis kept as a deprecated alias.administrators.createwithproduct_idsworks and keeps the role defaults whenpermissionsis omitted.administrators.permissions.updatereturns anETag.certificates.createwithsupersedes_certificate_idrevokes exactly that certificate.certificates.deleterecords the reason.- Diamond steps accept
3 hours, the default of one of the steps. - Responses carry
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Reset. A request body over 1 MiB is rejected with413even withoutContent-Length. - Other fixes: replies to questions published from a draft keep the requested
visibilityand checkIf-Match; copying a missing banner is404; attachments on a connected lesson are409; deleting an access schedule in use is409; custom fields that were deleted no longer appear on users;capabilitiesno longer reportsburst.
Document
- Every operation declares the
429response, the common response headers, andX-Client-Request-Id. Missing error responses were added.
3.3.2
September 24, 2026
Backward compatible with 3.3.1: no operation, path, field, or error code changed; only the tags that group operations in the documentation changed.
Salesis split intoGateways,Deliveries,SalesEvents, andSalesTransactions.Accountis split intoAccount,Domains, andReplicas.Settingsis split intoGeneralSettings,AccessSettings,EmailSettings, andSupportSettings.x-tagGroupsnow has separateAccountandSettingsgroups.
If your tooling filters operations by tag, update the tag names. Operation IDs and paths are unchanged.
3.3.1
September 24, 2026
Backward compatible with 3.3.0: no operation, field, or error code changed.
- The OpenAPI document now groups its tags with the
x-tagGroupsextension (Access, Students, Catalog and content, and so on), which documentation tools such as Redoc and Scalar use to organize the navigation. Tags themselves are unchanged.
No action is required.
3.3.0
September 23, 2026
Backward compatible with 3.2.0: no operation, field, or error code was removed or renamed. What changed is how existing operations affect replicas:
products.publications.createandmodules.publications.createalso publish the copies of the content in replica accounts. The previous description said publication did not reach replicas, which was incorrect: changes already propagated to replicas, asynchronously and not always completely. Propagation is now synchronous and complete. Replicated products and modules can also be published (previously403 replica_readonly).products.update,modules.update,showcases.update, andbanners.updateacceptstatuson replicated resources. Any other field still returns403 replica_readonly.products.copies.create,modules.copies.create, andlessons.copies.createreject a replicated source or destination with403 replica_readonly. The same applies tomodules.createon a replicated product and tolessons.updatewith a replicatedtarget_product_id. The API previously accepted these requests.showcases.delete: the description said the showcase's products were kept. Deleting a showcase has always deleted its products as well, and the description now says so.
No action is required. If your integration relied on API publications not reaching replicas, note that they now do.
3.2.0
September 23, 2026
No breaking changes since 3.0.0: everything that existed is still available, with the same name, the same type, and the same permission requirements.
- 196 new operations, completing the catalog for content (products, modules, lessons, exams, questions, taxonomies), users and access, support, sales and deliveries, Diamond funnel automations, imports, exports and reports, account settings, accounts and replicas, sandbox, event streams, and declarative configuration.
- Nine new asynchronous operation types, in an extensible enum: apply access in bulk, run a sandbox scenario, apply declarative configuration, apply delivery tags, reprocess a sales event, reprocess a funnel membership, analyze an import, process an import, and generate an export.
- New optional lesson field (
exam_id) to link an exam directly from the lesson. - Human mode (administrator OAuth plus credential) is available for credentials that require it. Authorization does not change: the credential's policy still applies.
If you are on 3.0.0 or 3.1.0, you do not need to do anything to keep working. To use the new features, call them: the base URL does not change.