Developers

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

  • label in POST /menus/{menu_key}/items and PATCH /menus/{menu_key}/items/{menu_item_id} now accepts at most 50 characters (it was 155). A longer label is validation_failed on the label field. 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_template in the configuration manifest: updates an email template, with id set to the template ID (sale, import, free, or new_access) and attributes in the request body format of PATCH /settings/emails/templates/{email_template_id}. Templates are a fixed catalog, so the manifest only updates them.
  • kind: custom_field in the configuration manifest: creates, updates, and deletes profile custom fields, in the request body format of POST and PATCH /settings/user-profile/custom-fields.
  • settings_manager in GET /credentials/policy-templates: every settings.update* permission, custom code, profile custom fields, and the configuration manifest (configuration.validate, configuration.plan, configuration.apply).

Changed

  • A settings item with an unknown id is still validation_failed, and the message now lists the accepted settings groups and points to kind: email_template and kind: custom_field.
  • The kind enum of ConfigurationManifest and the id enum of PolicyTemplateList are 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

  • timezone in GET /settings/platform and PATCH /settings/platform: the IANA time zone of the platform, such as America/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. Sending null returns 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

  • anonymous is no longer part of the Ticket schema (GET /support/tickets, GET /support/tickets/{ticket_id} and the expanded ticket of webhooks). No ticket was ever stored with the anonymous contact fields, so no response returned the block: a ticket without a user keeps user: 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/scores no 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_id of comment and question entries 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, and lesson_id could point to an unrelated lesson; those entries now return cause.kind without lesson_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_id now takes the public ID of the resource, the same value an event returns in resource.id (les_30, op_01J8Z3..., rul_lesson_5). Before, les_30 returned no events and only the internal number (30) matched.
  • resource_id requires resource_type. Without it, or with a value that is not a public ID of that resource type (such as the bare number 30), the request returns 422 validation_failed. Send the resource.object and resource.id of an event to find the other events about the same resource.

Fixes

  • PATCH /banners/{banner_id} keeps the visual settings of a netflix banner (title, text, colors, background, video and call to action, configured in the dashboard). Before, any change to name, type, link, images, html or showcases reset 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 login sent together with X-API-Key (what cademi-cli 0.3.x sends) returns 401 ambiguous_credentials, like any other OAuth access token together with a credential secret. cademi-cli 0.4.0 and later send the access token alone; run cademi 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_id of AuditEntry and RequestLog is the administrator who acted with their OAuth access token, and is null when a credential acted.
  • Capabilities describes the capabilities of the caller, a credential or the administrator of an OAuth access token.
  • The 3.15.0 entry below is corrected: the transition accepts the access token of cademi auth login sent together with X-API-Key by cademi-cli 0.3.x, recognized by the token, not by the CLI version; and the audit trail shows the administrator in actor.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>, without X-API-Key. The request acts as that administrator, with the administrator's own permissions (root and master_admin have 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 login is gone. During a transition, the access token of cademi auth login sent together with X-API-Key (what cademi-cli 0.3.x sends) is still accepted and acts as the administrator; the X-API-Key header 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-Key or as the bearer) returns 401 ambiguous_credentials. An access token whose administrator was removed, moved to another account or revoked returns 401 invalid_access_token.
  • A credential is always autonomous: auth_mode is no longer part of Credential, and POST /credentials with auth_mode returns 422 unknown_field. A credential that had auth_mode: human_required now authenticates with its secret alone, in Authorization: Bearer or in X-API-Key.
  • GET /credentials/current no longer returns human. With the OAuth access token of an administrator, GET /credentials/current and GET /credentials/current/policies return 404 not_found: no credential authenticated the request. Use GET /capabilities to read what the caller can do.
  • The error codes human_context_required, human_mode_not_allowed, admin_not_eligible and tenant_mismatch are 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_denied without the permission, 404 not_found outside its scope (an administrator limited to some products does not see the others). GET /capabilities lists them, and its limits.ops_per_minute is 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_exceeded applies to administrators as callers too: a full-access administrator grants anything, including administrators.* and credentials.*; any other administrator grants only what their own permissions grant, in POST /credentials, POST /administrators and PUT /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.kind of a processing attempt can be administrator (with an adm_ 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}/permissions returns role, full_access and policies instead of the permissions map and product_ids. Each item of policies is a Policy, the same object as GET /credentials/{credential_id}/policies; access limited to some products is the ids selector of product. root and master_admin return full_access: true and no policies.
  • PUT /administrators/{administrator_id}/permissions takes {role, policies}. Each policy is {capabilities, resources}, as in POST /credentials/{credential_id}/policies, and the policies replace the previous ones. Send policies: [] with root or master_admin; policies with them return 422 validation_failed. permissions and product_ids return 422 unknown_field.
  • revision and the ETag of both operations are the revision of the administrator's permissions. Send that ETag in If-Match. The ETag of GET /administrators/{administrator_id} is not accepted there anymore and returns 412 revision_mismatch.
  • POST /administrators takes policies instead of permissions. Without policies, the administrator gets the default permissions of the role, limited to product_ids when you send it. policies together with product_ids returns 422 validation_failed.
  • delegation_limit_exceeded means that nobody grants more than they have. A credential grants an administrator only the delegable permissions of its own policies, so never administrators.* or credentials.*; before, it could grant anything in the default permissions of the chosen role. Nobody changes their own permissions: that returns 409 state_conflict with details[].reason set to self_modification.
  • published_by of a Policy can 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.read and settings.update_appearance are 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 by GET /credentials/permission-catalog with introduced_in set to 3.14.0. catalog_version is 3.14.0.

Credentials

  • Administrators who are not root or master_admin can 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_status of GET /reports/exports is 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

  • attributes of a manifest item says which fields come from the path of the REST operation: module and lesson require product_id, which accepts {"$ref": "<ref>"}. A lesson also takes module_id and content. The other kinds take only the fields of the request body.
  • errors[].code of the validation report lists the codes of the manifest itself: required, cyclic_reference, reference_not_found, prohibited and order_set_mismatch. Errors in attributes use the validation codes of the corresponding REST operation. warnings is reserved and currently always empty.

Certificates

  • The 409 of POST /certificates lists the values of details[].reason: no_access, template_disabled, exam_not_passed and not_completed.

Reports

  • summary of GET /reports/exports declares total and by_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-attempts with mode=initial returns 409 feature_disabled with details[].reason set to first_import_requires_release. Contact support to release it; later imports are processed normally.
  • GET /webhooks/{webhook_id} returns 404 for a deleted webhook endpoint, as the list already did. It returned 200 with the full object before. If you read a webhook after deleting it, expect 404.
  • A credential granted administrator permissions on specific administrators (an ids selector) can update, delete, restore, change the permissions of and send access to those administrators. These operations returned 403 for them before. Administrators outside the selector and unknown IDs return 404, as before.
  • PATCH /sales/deliveries/{delivery_id} that archives a delivery already archived, or reactivates one already active, returns 200 with the delivery. Archiving an archived delivery returned 404 before.

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_id and filters.tag_id of POST /exports accept a single ID or a list of IDs, as the response already returned them. A list was refused with 422 unknown_field.
  • GET /exports, GET /exports/{export_id} and GET /reports/exports no longer list records without any date, which were never exports and returned created_at as null.

Responses that now match the document

  • operations.by_status and requests.by_outcome of GET /account/usage, diff of audit entries and request logs, and values of GET /users/{user_id}/custom-fields are objects. An empty value was returned as [].
  • type of a release rule and denial_reason of product access list null in their enum, as they were already nullable.
  • reissue of 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, use supersedes_certificate_id.

Other fixes

  • DELETE /automations/diamond/{diamond_id} on a Diamond with memberships returns details[].reason set to has_memberships.
  • GET /gamification/scores without 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-streams declares its 429 as either RateLimitError or the standard Error. 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 every 429 of this operation as RateLimitError, check error.code first.

Documented fields

  • UserAttribution lists its fields: utm_source, utm_medium, utm_campaign, utm_content, utm_term, page_name, adcampaign, groupid, adid, gclid, fbclid, cargo, nome_empresa and tamanho_empresa, each a string or null.
  • Items of GET /support/comments, GET /support/questions and GET /enrollments declare links.self, the canonical path of the item.
  • GET /users/{user_id}/custom-fields declares data.object (user_custom_fields).
  • ValidationFailed declares supported_kinds, returned with unsupported_resource.
  • Every operation declares 403 for refusals of the authenticated principal in human mode (admin_not_eligible, tenant_mismatch), under the new Forbidden response.
  • 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

  • user of a ticket is null when 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_adjusted and score_deleted progress adjustments, result.score_id and result.product_id are public IDs (sco_, prd_) instead of internal numbers. Read them as strings: score_id is the id of GET /users/{user_id}/scores/{score_id}, and product_id is null when the entry is not tied to a product. These two fields were not in the 3.12.0 document, so the release comparison did not report the change.

Progress adjustments

  • ProgressAdjustment.type lists every value the API returns: reset, exam_result_deleted, exam_attempt_reopened, score_adjusted and score_deleted. The enum is extensible, so new types may appear. result documents the keys of each type.

Gamification

  • The example of the score.deleted webhook shows a manual deletion, with exam_id set to null. exam_id and adjustment_id are never filled together.
  • The description of Score.origin says that a balance is also corrected by deleting a manual entry with DELETE /users/{user_id}/scores/{score_id}.
  • GET /credentials/permission-catalog reports introduced_in as 3.12.0 for scores.delete, the release that added it. catalog_version, in that response and in GET /capabilities, moves from 3.2.0 to 3.12.0 with it, and policies published from now on record 3.12.0.

Support

  • The description of CommentList no longer mentions include_replies, which is not a parameter of GET /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.removed is replaced by score.deleted. Deleting a resource is .deleted in every other event, and score.removed was the only exception. score.deleted is 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. data carries id of the entry, user_id, product_id, points, exam_id (null for a manual entry) and adjustment_id (null for an exam reversal). Webhook destinations and event streams subscribed to score.removed are moved to score.deleted by 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 by POST /users/{user_id}/score-adjustments or added in the dashboard. It requires reason, records the deletion as a progress adjustment, changes the balance by the negative of the entry's points and needs the new scores.delete permission. Points earned automatically return 422 score_not_removable.
  • Points added in the dashboard are now score adjustments, the same as POST /users/{user_id}/score-adjustments: they publish score.adjusted and are recorded as progress adjustments with their author and reason.

Support and sandbox

  • GET /support/comments declares its items as oneOf Comment or CommentReply, told apart by object. Replies were already listed since 3.11.0; only the document changed, so a client generated from the spec can read them.
  • The description of POST /sandbox/resets points to POST /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 (null when none). POST /support/departments and PATCH /support/departments/{department_id} accept it, and PATCH with null clears it. Renaming a department no longer clears the administrator.
  • DELETE /support/departments/{department_id} accepts unlink_tickets=true to unlink the department's tickets, including deleted ones, and delete it, as the dashboard does. Without it, a department with tickets is still refused with state_conflict.
  • Tickets carry queue: open awaits a staff reply, answered awaits the user, closed is closed. GET /support/tickets filters by queue, alongside status.
  • POST /support/tickets/{ticket_id}/replies accepts a reply with only attachments: text can be omitted when file_ids is present.
  • GET /support/comments also lists pending replies from students, and comment replies carry comment_id, the top-level comment. A student reply can be deleted individually with DELETE /products/{product_id}/comments/{comment_id}/replies/{reply_id}.

Deliveries and access

  • Release rules represent the closing date and the unit: params can carry closes_after_days and unit for relative rules and closes_on for date rules, and exam_id is the public exam ID (exm_).
  • PUT /sales/deliveries/{delivery_id}/products and PUT /sales/deliveries/{delivery_id}/rules describe that a delivery is live: changes apply to existing enrollments, except users with an individual override.
  • PATCH /users/{user_id}/products/{product_id}/access accepts duration: null to 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-permissions entries can carry any_of. Entries with the same any_of value are alternatives: any one of them grants access. Entries without any_of are all required. GET /reports/rankings is the first operation to use it: reports.read or rankings.read.

Deliveries

  • The request body of POST /sales/deliveries documents products with its own input schema, entries, the same shape as the body of PUT /sales/deliveries/{delivery_id}/products. It previously pointed to the response schema, which listed object and the read-only ignored_external_product_ids as required. Both are now optional and ignored when sent. Requests accepted before are still accepted.
  • PATCH /sales/deliveries/{delivery_id} explains that deleted is an alias of status and also requires deliveries.archive.

Settings

  • Two-step verification is now available to every administrator on every account, with no per-account switch. admin_two_factor_available in GET /settings/authentication is deprecated and always true; PATCH /settings/authentication still accepts it and ignores it. The field stays until v4.

Sandbox and support

  • POST /sandbox/resets and POST /sandbox/test-scenarios/{scenario_id}/runs explain that 403 sandbox_only and 409 sandbox_required both 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

  • limit declares its default of 50 in GET /users, GET /banners, GET /showcases, GET /showcases/{showcase_id}/products, GET /products/{product_id}/lessons and GET /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 ids needs 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}/replies states that replies are ordered by created_at and then ID, with no newest-first option.

Users

  • GET /users describes how access and deleted combine: access=none means users in the trash, and it does not test product enrollments.
  • POST /users documents custom_fields as a map of custom field IDs to values, and PATCH /users/{user_id} documents settings and its three restriction flags, including that sending the object replaces all three.

Files and usage

  • GET /account/usage explains that storage.used_bytes comes 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 /uploads as set by the account.

Webhook deliveries

  • A 400 answered with Queue is full. (the response Make sends when the queue of a webhook is full) no longer closes the delivery as failed. 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 records webhook.reschedule with reason destination_queue_full. Any other 400 is 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}/content and GET /products/{product_id}/modules/{module_id}/content accept limit (1 to 200, default 200) and cursor, and return next_cursor when 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 with 400 and the invalid_cursor error code.
  • GET /support/tickets/{ticket_id}/replies now documents limit (1 to 200, default 50), cursor and the 400 with invalid_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-Match header 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} carries x-cademi-manual-checks, an informative extension: changing name or content of a block whose type is pushalertco requires the custom_code.manage_pwa permission. 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_at is null. Only exports with status expired, generated while files were retained for 7 days, return the end of that period.
  • attachments[].url in 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 with POST /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.updated is 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 test in 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 configured payload_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 in POST /webhooks, PATCH /webhooks/{webhook_id} and the webhook kind of configuration manifests; returned by GET /webhooks and GET /webhooks/{webhook_id}, with IDs as public IDs.
  • An event is delivered only when it passes both resource_filters and the filters of its event type. Existing endpoints return event_filters: null and receive the same events as before.
  • Invalid filters fail with 422 validation_failed, and details[].field points to the event type or filter, such as event_filters.exam_attempt.completed.passed.
  • A PATCH that changes event_types without event_filters drops 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: true at the end of the envelope. It is signed like a real delivery, with the same headers, but holds sample data, its id and delivery_id do not exist in the API, and it is not retried. Real deliveries do not include the key.

Fixes

  • resource.id in GET /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 no id returned the internal number instead: lesson_progress.completed returned "30" and now returns les_30. This affects certificate_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 the rule_id of the payload), sales_event.reprocess_requested, score.removed, ticket.reply_created and user.access_updated. Webhook bodies have no resource object 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. Resource comment. 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. Resource question. 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, in POST /users, PUT /users/{user_id}/custom-fields or DELETE /users/{user_id}/custom-fields/{custom_field_id}. One event per field whose value changed; saving the value the field already has sends nothing. Resource user. Payload: id (the user), custom_field_id and filled (false when the value was cleared). The value is never sent: read it with GET /users/{user_id}/custom-fields.
  • user.first_accessed: the first access of a user to the student area. Sent once per user. Resource user. 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, with tag_ids as 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, and PUT /users/{user_id}/tags with the tags the user already has, publish no event.
  • The format did not change, and its version is still 1: tag_ids lists 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_detail is accepted by POST /webhooks and PATCH /webhooks/{webhook_id} and returned by GET /webhooks and GET /webhooks/{webhook_id}: ids (the default), masked or full. Existing endpoints are ids and receive the same body as before.
  • ids sends only data, which identifies people by ID. Its only free text is the note written by whoever operates the account (reason in certificate.revoked and diamond_membership.stage_changed), sent as written.
  • masked and full add an expanded object next to data, with the resources the event refers to (such as user, product and lesson) in the same format as the API returns them, as they were when the delivery was created. A key is null when 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, display and definitions are empty objects and offer is not included.
  • In masked, personal data is masked by fixed rules, now described in payload_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 the format of its field. In full, values are sent as stored.
  • Setting full, or changing the event types of a full endpoint, requires the users.read_personal permission, plus tickets.read_personal when an event type expands a ticket and legal_terms.read_proof when one expands a term_acceptance. Without them, the request fails with 403 and the new error code personal_data_permission_required, which configuration.apply can also return per item (OperationError).
  • When the credentials or the administrator that created a full endpoint lose one of those permissions, new deliveries are sent as masked until the permission is restored. GET /webhooks/{webhook_id} returns the level in use in health.effective_payload_detail.
  • A delivery keeps the expanded object built when it was created. After the level of an endpoint is lowered, a retry of an older delivery is sent without expanded. GET /webhooks/{webhook_id}/deliveries and GET /webhooks/{webhook_id}/deliveries/{webhook_delivery_id} return the level at which each delivery was created in payload_detail (ids when it has no expanded).
  • Every change of level is recorded in GET /audit-entries as webhook.payload_detail_changed, with the previous and the new level.

Typed event schemas

  • The OpenAPI document has a webhooks section with one entry per public event type, and one schema per type in components.schemas, such as LessonProgressCompletedEvent. Each schema describes the envelope, every key of data (type, description, example, whether it can be null and the prefix of each ID) and expanded, 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, error is 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 still 1. Events recorded before this change keep the original text until they expire.
  • validation_url of 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 reason in PATCH /users/{user_id}/certificates/{certificate_id} and PATCH /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_filters in POST /webhooks and PATCH /webhooks/{webhook_id} said that each key is the resource.type of the event. Webhook deliveries do not carry the resource of the event, and in GET /events the field is resource.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_filters in POST /webhooks and PATCH /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 returns 422 validation_failed on 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.completed is about a lesson, so a product filter does not restrict it; list the lessons under lesson instead. Events whose resource type is not a key of resource_filters are delivered without restriction. This was already the behavior in 3.6.0.
  • PATCH /webhooks/{webhook_id} with an http://localhost URL in a sandbox account returns 422 validation_failed on url, like POST /webhooks. It returned 500. The description of POST /webhooks no longer says that http://localhost is 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: in GET /automations/diamond/{diamond_id}/triggers and GET /automations/diamond/{diamond_id}/triggers/{trigger_id}, in trigger_id of GET /automations/diamond/{diamond_id}/stages, and in the diamond_trigger.updated event. 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 with GET /automations/diamond/{diamond_id}/triggers and match them by stage_id. An old ID returns 404, and an If-Match taken before the deploy returns 412.

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. Resource lesson. Payload: user_id, product_id, lesson_id.
  • product_progress.started: a user completed the first lesson of a product. Resource product. Payload: user_id, product_id.
  • product_progress.completed: the progress of a user in a product reached 100%. Resource product. Payload: user_id, product_id.
  • product_progress.milestone_reached: the progress crossed one or more milestones (50, 75, 90) with a single completed lesson. Resource product. 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. Resource product. Payload: user_id, product_id, percent (integer from 0 to 100, the same value as percent in GET /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.
  • editor and document: 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 filename without an extension or with an extension outside the list returns 422 validation_failed on filename (code extensions); a size_bytes above the limit returns 422 validation_failed on size_bytes (code max). 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 failed with failure_reason too_large, and the API returns 422 validation_failed on size_bytes. A session whose filename extension is outside the list of its purpose fails the same way, with failure_reason extension_not_allowed and 422 validation_failed on filename.

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-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