Changelog
3.4.7
September 25, 2026
Current release of the contract. 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.