Changelog

3.4.7

September 25, 2026

Current release of the contract. Backward compatible with 3.4.6.

  • If-Match on lesson attachments (update and delete) is checked atomically with the write: of two concurrent writes with the same ETag, one succeeds and the other returns 412 revision_mismatch. DELETE of an attachment now accepts If-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_conflict with reason no_failed_run.
  • A malformed public ID in the request body returns 422 with code invalid and a message that names the expected resource.
  • A failed item of an asynchronous operation always has code and message, and a failed operation always has error (the first failed item, or items_failed).
  • 409 already_exists for an email in use points to field: email.
  • An unreadable import file returns the documented message instead of the parser error.
  • 422 idempotency_key_reused tells a key reused for a different resource (reason different_resource) from a key reused with a different body (reason different_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 500 with valid input now return the documented error: public IDs above the supported range (404, or 422 when 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 an external_id already in use (409 already_exists on external_id). Legacy point records no longer break score listings.
  • An ID in the request body that cannot be parsed is 422 on its field. It was stored as an invalid reference in some module and lesson moves.
  • Changes to replicated content are rejected with 403 replica_readonly consistently.
  • Domain rejections have their own codes and reasons: a user without access to the product (409 state_conflict, reason no_access), a revoked enrollment (reason revoked), a certificate or exam result that no longer exists (404), and a reason in every 409 state_conflict that has more than one cause.
  • Every item in details has a code and a message in the language of the request, and domain_rule_violation explains the rule.
  • Asynchronous operation items report failures with code, message, and field or reason, described by the new OperationError schema, instead of a generic not_found.
  • Reusing an Idempotency-Key for a different resource or a different body returns 422 idempotency_key_reused in every state. A malformed If-Match returns 400 if_match_invalid; If-Match: * matches any existing revision. A JSON body that is not an object returns 400 malformed_json.
  • The document declares the common error responses (406, 409 idempotency conflicts, 400, 413, and 415 where they apply) and the page object 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 500 now succeed or return the documented error: creating a user with an @sandbox.invalid email (and an email with an unknown suffix is 422 on email), 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/security only 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 4xx error has its own message, and every 422 validation_failed lists its details.
  • Webhook delivery attempts expose id; the certificate template update declares its own request body; custom code declares the keys of content; purpose=import uploads 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 details of 422 unknown_field and 400 unknown_parameter now carries code and message in addition to field or parameter, as the error guide describes.
  • Validation messages in details[].message are translated, in the language of the response (Accept-Language). They were returned as raw keys such as validation.in.
  • POST /event-streams accepts start: earliest (the default, the previous behavior) delivers the retained events from the beginning; now delivers 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 204 declares its body. Support comments, questions, tickets, departments, and FAQs now reference their schemas, with new list schemas for the collections.
  • event_streams.events.get declares its 200 as text/event-stream, marked with x-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.list and reports.rankings.list accept product_id[], like the other array filters. product_id is kept as a deprecated alias and now also accepts a single value.
  • EventStream.filters is 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 params fields of release rules and of the release calendar are declared as objects. They were serialized as [], which is not a valid schema.
  • The scopes of 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 is 404, identical to a resource that does not exist. A reference in the request body that is out of scope (delivery_id in users.enrollments.create, product_id in tickets.create) is 422 on that field.
  • deleted in PATCH accepts only false (restore). true was accepted and ignored; it is now 422. Use the DELETE operation to delete.
  • showcases.create places the new showcase after after_id, as the name says. It was placed before it. An after_id that does not exist is now 422.
  • administrators.update no longer accepts status, which had no effect.
  • files.list accepts only sort=-created_at, the order it always used.
  • uploads.create accepts only the upload purposes image, pdf, import, editor, and document. export is reserved for files the platform generates.
  • sandbox.resets.create called with production credentials returns 403 sandbox_only. The production_only code is removed.
  • In declarative configuration, lessons use name, like the REST body. title is now an unknown field.
  • Idempotency-Key is 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 missing next_cursor.
  • position takes 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 without position places it last.
  • users.list applies status, access, created_after, and created_before.
  • files.download_links.create applies inline and download_name.
  • exports.create with user_activity requires a valid user_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, reason plan_already_applied); a masked secret sent back is 422 (reason masked_secret); order_set_mismatch is reported by validation with 200 and valid: false.
  • Settings and email templates have their own revision: a change to one group no longer invalidates the ETag of another.
  • credentials.update accepts suspended, the status the API returns; inactive is kept as a deprecated alias.
  • administrators.create with product_ids works and keeps the role defaults when permissions is omitted. administrators.permissions.update returns an ETag.
  • certificates.create with supersedes_certificate_id revokes exactly that certificate. certificates.delete records the reason.
  • Diamond steps accept 3 hours, the default of one of the steps.
  • Responses carry RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. A request body over 1 MiB is rejected with 413 even without Content-Length.
  • Other fixes: replies to questions published from a draft keep the requested visibility and check If-Match; copying a missing banner is 404; attachments on a connected lesson are 409; deleting an access schedule in use is 409; custom fields that were deleted no longer appear on users; capabilities no longer reports burst.

Document

  • Every operation declares the 429 response, the common response headers, and X-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.

  • Sales is split into Gateways, Deliveries, SalesEvents, and SalesTransactions.
  • Account is split into Account, Domains, and Replicas.
  • Settings is split into GeneralSettings, AccessSettings, EmailSettings, and SupportSettings.
  • x-tagGroups now has separate Account and Settings groups.

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-tagGroups extension (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.create and modules.publications.create also 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 (previously 403 replica_readonly).
  • products.update, modules.update, showcases.update, and banners.update accept status on replicated resources. Any other field still returns 403 replica_readonly.
  • products.copies.create, modules.copies.create, and lessons.copies.create reject a replicated source or destination with 403 replica_readonly. The same applies to modules.create on a replicated product and to lessons.update with a replicated target_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.

On this page