變更記錄
Changelog
Added, changed, deprecated and removed behavior across the Management API, Content Delivery API and Customer API, newest first.
2026-11-04
Localization: automated emails follow the customer's language
Management API
- addedlocalization.translations.* accept message_template: the id of an automated email whose content the owner customized. Its translation is what customers in that locale receive; writes go live immediately and need the permission of the email's scenario group (commerce:manage, marketing:manage or customers:manage). A translation may only use template variables (the triple-brace placeholders) that appear in the source text.
- changedAutomated emails go out in the customer's recorded locale when the site serves it. Emails the owner never customized use built-in wording for every site language (previously six). A customized email without a translation in the customer's locale goes out in the primary locale.
- changedstore_settings translations default entityId to the website id, like global_sections and site_settings. localization.translations.translate with version draft on a record without its own publishing (products, forms, courses, customized emails) now puts the translation live, as a person's correction already did.
2026-11-04
Localization: publishing one locale needs the record's publish permission
Management API
- changedlocalization.translations.publish for pages, global sections and site settings now needs site:publish, for blog posts blog:publish and for collection items collections:publish. Members who can only edit those records can still write translation drafts; they go live when the record is published or when someone with publish access publishes the locale. Other record types are unchanged.
2026-11-04
Localization: more translatable fields (product details, variants, lessons, quizzes, store notes)
Management API · Content Delivery API · Customer API
- addedlocalization.translations.* accept two more entity types: pricing_tier (variant description and highlight badge) and store_settings (store-wide processing time and purchase notes; entityId is the website id). Products add detailMarkdown, faq[].question, faq[].answer and purchaseNotes; course units add body.markdown and quiz body.questions[].prompt, options[].text and explanation.
- changedDelivery product reads return translated long-form details, FAQs, variant descriptions, processing time and purchase notes. Customer API course trees return translated text lessons and quiz questions; POST /units/{id}/quiz accepts locale and returns translated explanations when answers are revealed. Grading always uses the primary answer key.
2026-11-04
Localization: each locale can have its own URL slug
Management API · Content Delivery API
- addedlocalization.translations.upsert accepts slug (one lowercase segment; null follows the primary slug). Changing a live slug adds a 301 from the old URL in that locale. New error INVALID_SLUG; SLUG_TAKEN now also covers slugs that would hide another record in that locale.
- addedDelivery reads by slug accept any locale's slug and return slug in the requested locale. Pages and products return localePaths (path in every live locale, without the prefix); collection items return localeSlugs. GET /redirects rules carry locale: rules with a locale match the full path including the prefix.
- changedThe first machine translation into a locale generates that locale's slug from the translated title (Latin-script languages get an ASCII slug; other scripts keep an ASCII primary slug). Site links, sitemap, hreflang, search results, llms.txt and JSON-LD use each locale's slug.
2026-11-04
Products: access duration is set per variant
Management API
- addedpricingTiers[].accessDays on products.get / products.list: how many days buyers of that variant can access every deliverable it grants, counted from payment (null = permanent). One product can sell a 3-month variant and a lifetime variant.
- addedproducts.update accepts pricingTiers: [{ id, accessDays }] to set the access duration of existing variants. Only accessDays can be changed this way; variants not listed are left unchanged.
- removedaccessDays on deliverable links: products.* deliverables[] and deliverables.get products[] no longer include it, and products.update({ deliverables }) now fails with VALIDATION when a link carries accessDays. Existing link durations were moved to the variants they applied to (the longest one when several applied; permanent counts as the longest).
- changedPurchases unlock deliverables for the variant's accessDays. A custom-amount purchase, which has no variant, uses the longest accessDays among the product's variants.
2026-11-03
Promotions: create and manage promotions through the API; selector-based targeting
Management API
- addedpromotions.get, promotions.create, promotions.update (PATCH), promotions.archive, promotions.unarchive and promotions.delete. The same rules as the dashboard apply: discount codes are unique per site, FLASH_SALE needs endsAt, and every referenced product, variant, category, booking resource or market must belong to the site (VALIDATION with details.code).
- addedTargeting is a selector evaluated against checkout lines instead of a product whitelist: kinds (PRODUCT, BOOKING, CUSTOM), refs (catalog item id, with variantId for products), categoryIds and priceRange. Bookings and custom items can now be in scope, including for minimum-order discounts. marketIds limits a promotion to some markets; audienceType RETURNING takes minPriorPurchases.
- changedpromotions.list returns the full promotion (selector, marketIds, amounts, limits, audience, archivedAt) and accepts includeArchived. The kind and discountType values are PRODUCT_DISCOUNT, DISCOUNT_CODE, QUANTITY_DISCOUNT, FLASH_SALE, MIN_ORDER and PERCENTAGE, FIXED_AMOUNT, OVERRIDE_PRICE; the previously documented ORDER_DISCOUNT and FREE_SHIPPING never existed.
2026-11-03
Contacts: external references, metadata, sign-in status and contact webhooks
Management API
- addedcontacts.add and contacts.update accept externalId / externalIdOrigin (your system's id for the person, unique per site, set once; never used to match or merge contacts) and metadata (flat key/value integration fields, merged on update, null deletes a key). contacts.get can look a contact up by externalId instead of contactId.
- addedcontacts.get returns accessStatus (ACTIVE or BLOCKED: a blocked person cannot sign in to the customer account; orders and emails are unaffected), externalId, externalIdOrigin and metadata.
- addedWebhooks customers.contact.created, customers.contact.updated, customers.contact.merged, customers.contact.access_changed and customers.consent.updated. Payloads carry ids and field names only, never the email address or phone number.
- changedCustomer API POST /auth/verify returns ACCOUNT_BLOCKED when the owner has blocked the person's sign-in. POST /auth/challenge answers the same way as for an unknown address, so the sign-in page cannot be used to find out who is blocked.
2026-11-03
Localization is generally available; site export includes locales and translations
Management API
- changedThe localization capability domain is now available for hosted and headless sites alike: locale set, full-site machine translation, per-locale delivery, SEO files, webhooks, SDK helpers and customer-facing text. The product page at /developers/products/localization describes the contract.
- addedThe site export (sites.export) now includes localization/locales.json (the locale set with status, URL prefix and auto-translate flag) and localization/translations.json (every translation row with draft and published field data, translated slug and sourceHash).
2026-11-03
Customer API: course, unit and order text in the customer's locale
Customer API
- addedGET /customer/deliverables, GET /customer/deliverables/{id}, GET /customer/orders, GET /customer/orders/{id} and POST /customer/units/{id}/open accept locale (a live locale's code or URL prefix). Course and unit titles and descriptions, the opened unit's title and order line names are served in that locale; untranslated text falls back to the primary locale. Without locale, Kanorio uses the customer's own locale (set by their last checkout, sign-in or form submission). Deliverable responses on multilingual sites carry resolvedLocale.
- changedCompletion certificates are rendered in the learner's locale (text and the translated course title) instead of always the site's primary locale. <KanorioUnit> passes the provider's locale when loading a course.
2026-11-03
SDK: locale routing, hreflang alternates and wildcard redirects at the edge
Management API · Content Delivery API
- added@kanorio/sdk: buildAlternates({ siteUrl, site, path, requestLocale, resolvedLocale, availableLocales }) returns canonical, hreflang languages and <link> entries with the hosted site's rules; requestLocaleOf(pathname, site) resolves the locale from a URL prefix; localizedPath and languageSwitcher build the same page in another locale.
- changed@kanorio/sdk/edge createKanorioMiddleware: on multilingual sites (features.locales, on by default) the primary locale's own prefix redirects 301 to the root path, /{prefix}/llms.txt, /{prefix}/llms-full.txt and /{prefix}/rss.xml are served from the Content Delivery API with that locale, redirect rules match the path without its locale prefix and keep the prefix on relative targets, and evaluate() reports the request's locale. Redirect rules now also match /* wildcard prefixes (exact match first, then the longest prefix), the same as the hosted site.
- addedsite.check_external_frontend returns three more checks on multilingual sites: i18n_pages (secondary-locale pages reachable under their prefix with the translated title), i18n_alternates (hreflang on the sampled primary-locale page) and i18n_files (/{prefix}/llms.txt).
2026-11-03
Cache tags: per-record and per-locale invalidation, new localization events
Management API · Content Delivery API
- changedContent events no longer carry site:<id> in data.cacheTags (content.page.*, collections.item.*, collections.items.bulkChanged, collections.schema.changed): a single record going live refreshes that record and its lists, not the whole site. Keep tagging fetches with the response's X-Kanorio-Cache-Tags; a front end that only uses the site tag should subscribe to content.site.published. createRevalidateHandler no longer adds the site tag on its own.
- changedX-Kanorio-Cache-Tags is declared by the server that builds the response: single pages list their breadcrumb ancestors (page:<id>), single items list expanded references (item:<id>, collection:<key>), and single records no longer carry the list tag (collection:<key> / pages are on list responses only; single items carry collection:<key>:items, which collections.schema.changed sends). On a secondary locale every tag is repeated with @<locale> and the response adds locale:<code>.
- addedcontent.translation.published and content.translation.deleted fire when one record's translation in one locale changes on its own, with cacheTags limited to that locale (item:<id>@fr-FR, collection:blog@fr-FR, …). localization.locale.published / unpublished / updated fire when the locale set changes (cacheTags site:<id>), and localization.job.completed / failed when a full-site translation job ends (completed sends locale:<code>). Existing content events now include data.locale.
- changed@kanorio/sdk: createRevalidateHandler prefixes every tag with kanorio: before calling revalidateTag (set tagPrefix to "" for raw tags); tag your fetches with the new cacheTag.* builders, localizeCacheTags or frameworkCacheTags(res.headers) so both sides match. parseDeliveryCacheTags still returns the raw tags.
2026-11-03
Multilingual sites: locales, translations and translation jobs
Management API
- addedlocalization.locales.list, add, update, reorder, remove and set_primary (GET and POST /sites/:id/locales, PATCH and DELETE /sites/:id/locales/:locale, PUT /sites/:id/locales/order, PUT /sites/:id/locales/primary) manage locales: status (DRAFT or PUBLISHED), auto-translation, URL prefix and the primary locale (single-locale websites only). add, remove and set_primary require confirm: true and are safe to retry. A new locale with autoTranslate (the default) is translated by a full-site job and goes live when it finishes. Taking a live locale offline or removing it 301-redirects its prefix to the primary locale.
- addedlocalization.translations.list, get, upsert, publish, delete and translate (/sites/:id/translations/:entityType/:entityId/:locale) read a record's translation with its source fields (path, text, profile, fingerprint, and missing / current / outdated), write translated fields against the source fingerprint (409 SOURCE_HASH_MISMATCH when the source changed), put a draft translation live without publishing the record, and machine-translate one record now (counts toward the daily translation limit; 429 DAILY_LIMIT_REACHED). Fields written through the API are recorded as AGENT or EXTERNAL and machine translation never overwrites them. Translating needs content:edit plus the record type's own permission.
- addedlocalization.jobs.list, get, retry and cancel (/sites/:id/translation-jobs) follow, re-run and cancel a locale's full-site translation job. A job translates only missing or outdated records and pauses at the daily limit instead of failing.
2026-11-03
Delivery API: locale on every read
Content Delivery API
- addedEvery read route (pages, globals, collections and items, products and catalog, feeds, search and the SEO derivatives) accepts locale, either a locale code in any letter case or the locale's URL prefix, and returns that locale's published translations; untranslated fields fall back to the primary locale. Pages, items and products carry resolvedLocale, and on multilingual sites availableLocales. Only live locales are accepted (400 UNSUPPORTED_LOCALE otherwise), and the primary locale shares the cache entry of an omitted locale.
- addedGET /api/v1/search takes locale to search one live locale's translated text and URLs, and total=true to also return the match count.
- changedGET /api/v1/seo/llms.txt, /seo/llms-full.txt and /seo/jsonld take locale; sitemap.xml lists every live locale's URLs with hreflang alternates.
2026-11-03
Products: images and description are optional
Management API
- changedproducts.create and products.update no longer require images or description. images may be an empty array (the product page renders without a gallery; listings, cart and checkout show a placeholder; the share image falls back to the site's; JSON-LD omits image) and description may be an empty string. Validation errors coverImageRequired and descriptionRequired are no longer returned.
2026-11-03
Developer settings: headlessEnabled removed
Management API
- removedheadlessEnabled is removed from the input and output of site.update_developer_settings (deprecated since 2026-10-22). Send frontendMode ("HEADLESS" or "HOSTED") instead and read frontendMode from the response. A request that sends only headlessEnabled now fails with VALIDATION; headlessEnabled sent next to other fields is ignored.
2026-11-03
Site brand identity: one setting for hosted pages, checkout, sign-in, emails and your own front end
Management API · Content Delivery API
- addedDelivery GET /site returns brand: displayName, logo per backdrop (light / dark), colors.primary per color scheme with a readable foreground, the owner's colorScheme (system, light or dark), cornerRadius plus the radius scale, typography and the fonts it uses with stylesheet URLs. Values are resolved, so use them directly; @kanorio/elements applies them automatically.
- changedsite.get_settings returns brand (displayName, logo, brandColor, brandForeground, typography, cornerRadius, colorScheme) and theme (sectionDensity, textSizeSettings). site.update_settings writes brand.brandColor, brand.brandForeground, brand.typography, brand.cornerRadius and brand.colorScheme, which take effect immediately without site.publish, and theme fields, which stay draft until site.publish. brandColor, brandForeground, cornerRadius, typography and colorScheme are no longer read or written at the top level of settings: a request that sends only those fails with VALIDATION, and next to other fields they are ignored. The CLI flag --color-scheme now writes settings.brand.
- changedBrand identity applies to every site regardless of frontendMode, so a HEADLESS site's checkout, sign-in and customer account now carry its colors, fonts, corner radius and color scheme without publishing.
2026-11-02
Products: shipping is decided per variant
Management API
- addedpricingTiers[].requiresShipping on products.get / products.list: whether buyers of that variant enter a delivery address and pay shipping. Variants without their own setting follow productType, so one product can mix a course-only variant and a course + printed book variant. Order lines snapshot the result as productType (PHYSICAL when shipped, DIGITAL when not).
- changedproductType is now the default for variants and can be changed with products.update; PRODUCT_TYPE_LOCKED is no longer returned. Changing it only affects future checkouts. Omitting productType on update leaves it unchanged.
2026-11-02
Deliverables: units no longer have plan scope
Management API
- removedappliesToAllTiers and applicableTierKeys are removed from deliverable units (deliverables.get, deliverable_units.replace_tree, deliverable_units.update). Every unit is available to everyone who has the deliverable; sending these fields now fails with VALIDATION. To give a higher plan extra content, put it in a separate deliverable and link it only to that plan with products.update({ deliverables: [{ deliverableId, tierId }] }). Plan-scoped units could never be completed by learners on lower plans, because completion counts every unit.
2026-11-02
Deliverables: certificate background image
Management API
- addedsettings.certificate gains backgroundAssetId (public image asset, centred and cropped to fill the A4 landscape page; the default border is not drawn when set) and backgroundOverlay (semi-transparent white layer over the background, default false). Both are required when settings.certificate is sent to deliverables.create or deliverables.update, like the other certificate fields; read the current values with deliverables.get first.
- fixedCertificate signature and background images now count as in use in the media library, so they are no longer listed as unused or cleaned up while a course still prints them.
2026-11-02
Validation issues in error.details.issues; MCP server 5.0.0
Management API · Content Delivery API
- changedVALIDATION errors from the Management API and MCP put the failing fields in error.details.issues ([{ path, message, code }]), the same place the Content Delivery API uses. error.details used to be the bare array.
- fixedThe Content Delivery API OpenAPI document no longer lists ok on POST /forms/{id}/submit, POST /forms/{id}/upload-request, POST /units/{id}/open and the analytics beacons; the responses had already dropped it. POST /forms/{id}/upload-request now declares FILE_TOO_LARGE, FILE_TYPE_NOT_ALLOWED and UPLOAD_UNAVAILABLE.
- fixedThe documented request body of POST /forms/{id}/submit now matches what the endpoint reads: consent is "checked" or "submit_statement", and bot protection is sent as _hp, _wp, _co, _ts, _tk (nonce|ts|sig from GET /forms/{id}/challenge, or an ALTCHA payload) and _ctx. GET /collections/{key}/items documents total as default false; it was already opt-in.
- changed@kanorio/sdk forms.client follows the unified contract: requestUpload and submit return { ok: true, … } or { ok: false, error: { code, message, issues? } } read from the error envelope (they used to look for an ok field the API no longer sends). New protectionFromChallenge(formId, challenge, renderedAt) builds the protection fields.
- added@kanorio/sdk exports paginate() and listAll(), which follow nextCursor across any list endpoint of the three APIs.
- changedBreaking for MCP clients that cached tool schemas: list tools return { data, hasMore, nextCursor } (see the other entries of this date). The MCP server version is now 5.0.0.
2026-11-02
Customer API: no ok wrapper, unified errors, paged lists, generated OpenAPI
Customer API · Content Delivery API
- changedSuccessful Customer API responses no longer carry ok: true. Errors use { error: { code, message, details? } } like the other two APIs; retryAfter and availableAt moved into error.details. VALIDATION_ERROR is now VALIDATION.
- changedGET /deliverables and GET /orders return { data, hasMore, nextCursor } and take limit and cursor (GET /orders no longer takes offset). GET /reviews returns the statuses in the same envelope, all in one page.
- changedContent Delivery API write endpoints follow the same shapes: POST /forms/{id}/submit returns {} or { state }; POST /forms/{id}/upload-request returns the ticket without ok and fails with FILE_TOO_LARGE (413), FILE_TYPE_NOT_ALLOWED (400) or UPLOAD_UNAVAILABLE (503); POST /units/{id}/open returns the open result without ok; INVALID_ANSWERS lists field errors in error.details.issues.
- changed@kanorio/sdk: KanorioApiError.retryAfter and availableAt are read from the Retry-After header or error.details; the { ok: false } parser is gone. createCustomerContentClient still returns { ok: true, … } or { ok: false, code } results for Elements.
- addedThe Customer API OpenAPI document is generated from its endpoint registry (x-kanorio-domain, x-kanorio-errors, x-tagGroups), like the Management API and Content Delivery API.
- changedLOCKED is 403 on every API (automations actions and flows used to return it as 422).
2026-11-02
Content Delivery API: list envelope, unified errors, generated OpenAPI
Content Delivery API
- changedList endpoints return { data, hasMore, nextCursor } and take limit (1-100, default 20) and an opaque cursor: /pages, /collections/{key}/items, /products, /reviews and /search (limit 1-20). /redirects and /collections return everything in one page with hasMore false.
- removedoffset on /collections/{key}/items and page on /products. /collections/{key}/items no longer returns total by default; pass total=true to get it.
- changedErrors always use { error: { code, message, details? } }; retryAfter and issues moved from error into error.details. INVALID_CURSOR and INVALID_LIMIT (400) are new.
- changedSEARCH_BUSY on /search is now 429 with Retry-After, the same status as on the Management API (it used to surface as 503 UPSTREAM_UNAVAILABLE).
- addedThe OpenAPI document (/api/v1/openapi.json) is generated from the endpoint registry and carries x-kanorio-domain, x-kanorio-errors and x-tagGroups like the Management API.
2026-11-02
Lists: one pagination contract; errors declared per operation
Management API
- changedEvery list operation returns { data, hasMore, nextCursor } and takes limit (1-100, default 20) and an opaque cursor (pass the previous page's nextCursor). The resource-named arrays (sites, posts, items, contacts, …) and total, page, pageSize and offset are gone. Extra metadata stays as sibling keys: products.list storeCurrency, media.list usage, submissions.list attachments, site.search q and tokens, globals.list_headers assets and contentUpdatedAt, webhooks.list subscribableEvents and quota.
- removedThe offset, page, pageSize and perPage parameters on every list. Lists that never had paging (sites, pages, forms, segments, campaigns, promotions, authors, blog categories and tags, product categories, collections, automations.flows) are now paged too.
- changedSmall bounded lists (automations.list, automations.templates.list, automations.triggers.list, automations.secrets.list, domains.list, webhooks.list, globals.list_headers, imports.list, media.list_page_images) use the same envelope and return everything in one page with hasMore false.
- changedimports.get returns rows as a page ({ data, hasMore, nextCursor }) with limit and cursor; analytics.bots breakdown paging uses an opaque string cursor and returns hasMore.
- addedINVALID_CURSOR (400) when a cursor is malformed or was issued for different filters. Collection item queries used to restart from the first page silently; they now return this error on both the Management API and the Content Delivery API.
- addedEvery operation lists the error codes it can return in x-kanorio-errors (code, HTTP status, note), and its error responses only include the statuses that can happen.
2026-11-02
Management API: one name, one base URL, capability domains on every operation
Management API
- changedThe API is now named the Management API everywhere (OpenAPI info.title is "Kanorio Management API"). MCP at /api/mcp is the MCP transport of the same API, not a separate one.
- changedThe canonical base URL is https://app.kanorio.com/api/v1. The servers entry and SDK default previously said https://kanorio.com/api/v1, which never served the API.
- removed@kanorio/sdk: createAgentClient, DEFAULT_AGENT_BASE_URL, AgentClient, AgentClientOptions, AgentPaths and the @kanorio/sdk/agent subpath. Use createManagementClient, DEFAULT_MANAGEMENT_BASE_URL, ManagementClient, ManagementClientOptions, ManagementPaths and @kanorio/sdk/management. No aliases are kept.
- addedEvery operation and tag carries x-kanorio-domain (the capability domain it belongs to), and the document has x-tagGroups that group resources by domain.
- changedx-kanorio-rate-limit labels are now default (120/minute per token), heavy (20/minute per token, plus 20/minute per site) and publish (30/hour per token).
2026-11-02
Deliverables: require-in-order is now enforced; quiz_passed rejected until quizzes ship
Management API
- changedsettings.requireSequential is enforced by the access layer: a unit stays locked until the previous required unit is completed. Customer API unit access gets reason prerequisite with lockedByUnitId; POST /units/:id/open answers 403 LOCKED; entitlements.check returns the same reason. Free-preview units are exempt.
- changeddeliverables.create / deliverables.update reject settings.completionRule { type: "quiz_passed" } with VALIDATION: learners cannot take quizzes yet, so the rule could never be met. It returns when quiz answering ships. Deleting the quiz unit a stored rule points to resets the rule to all_units.
- addedUnits have countsTowardCompletion (false = bonus content: not counted in percent or completion, never locked by require-in-order and never a gate) and isPrerequisite (everything after the unit stays locked until it is completed, with or without require-in-order). Both are on deliverable_units.replace_tree / deliverable_units.update, in deliverables.get, in the Customer API tree and in the public outline on products.
- changedCompletion is now a snapshot: the first time a learner meets the completion rule a DeliverableCompletion row is written and never reverts, so adding units later does not un-complete anyone and deliverables.progress.completed fires once. Customer API progress and Agent progress.list return completedAt. Percent and per-unit status are still live.
- addedWebhook deliverables.progress.unit_completed fires when a learner completes one unit (contactId, deliverableId, unitId); deliverables.progress.completed is unchanged.
- changedText, PDF, attachment and embedded-video units are no longer completed just by opening them: the hosted player and <kanorio-unit> show a Mark as complete button (POST /progress { completed: true }). Video and audio still complete automatically at 90%. Units locked by require-in-order still return body/embed to an entitled learner so the player can unlock them locally once the previous unit is done; files still go through POST /units/:id/open, which re-checks on the server.
- addedQuizzes: a quiz unit's body is { questions: [{ id, type: single|multiple|true_false, prompt, options: [{ id, text, correct }], explanation? }], settings: { passingPercent (default 70), maxAttempts (null = unlimited), showAnswers: never|after_pass|always } } and is validated on write (UNIT_TREE_INVALID quiz_body_invalid). Learners answer with Customer API POST /units/:id/quiz { answers }: the server grades, records the attempt, completes the unit on a pass and returns score, attempts left and (per showAnswers) the correct options. The tree exposes a quiz summary per unit and never the correct answers; POST /progress cannot complete a quiz unit. settings.completionRule quiz_passed is accepted again.
- addedCertificates: settings.certificate { enabled, title, signatureName } now issues a PDF certificate (serial, issuedAt) the first time a learner completes the deliverable; Customer API GET /deliverables/:id/certificate returns a signed download URL and the tree carries certificate { enabled, serial, issuedAt }. Webhook deliverables.certificate.issued.
- addedprogress.reset and progress.complete (customers:manage): reset one learner's progress, quiz attempts and completion record, or mark them complete on their behalf (completes required units, writes the completion record, issues the certificate when enabled). progress.list rows add startedAt, lastActivityAt and certificate { serial, issuedAt } | null.
- addedWebhook deliverables.quiz.attempted (contactId, deliverableId, unitId, attemptNo, score, passed).
2026-11-01
Learning insights: deliverables.insights, deliverable_units.insights, viewing fields on progress.list
Management API
- addeddeliverables.insights (store:view; GET /v1/sites/:id/deliverable-insights?deliverableId=&days=|from=&to=) returns the same figures as the course overview in the console: learner statuses, stalled and expiring counts, completion rate, median days to complete, quiz pass rate, certificates, period activity with daily actives, the course outline (reached and completed per unit, biggest drop) and a per-course comparison table when deliverableId is omitted. Figures that do not apply are null, never 0.
- addeddeliverable_units.insights (store:view; GET /v1/sites/:id/deliverables/:deliverableId/unit-insights?unitId=&days=) returns per-unit plays, viewers, watched seconds, average share watched, quick-exit rate, stall rate, fast-playback share, quiz pass rate with the most-missed question, ranked symptoms, and with unitId the 100-bucket retention, rewatch and seek curves.
- changedprogress.list rows now include watchedSeconds, lastPlayedAt and unitCoverage (share of each media unit actually watched, 0-100).
2026-11-01
Authors and instructors: one shared list for posts and deliverables
Management API
- changedThe system collection blog_authors is renamed authors: post authors and deliverable instructors are the same people. The old key keeps working as an alias in the Delivery API and collections.* / items.* (/api/v1/collections/blog_authors/items still returns the list). A site that already had its own custom collection named authors keeps its system collection under blog_authors. Webhook item events for this collection now carry collectionKey authors.
- addedAuthors have a title field (job title or credential, e.g. Head Chef), in the authors collection, the authorIds of deliverables.* and the authors[] of the Customer API deliverable tree and the public deliverable outline on products.
- addedauthors.list, authors.create and authors.update manage the shared list (name, title, avatarAssetId, bio, url) so an agent can create a course and its instructor in one go; attach them with deliverables.update { authorIds } or posts.update { authorIds }.
- addedDelivery API GET /api/v1/products accepts author=<slug>: only products linked to an active deliverable this person teaches. With the authors collection item and the posts author filter it gives a headless instructor page everything it needs.
- changedA hosted blog author page only exists for people with at least one published post; people who only teach courses get no empty author page (the sitemap already listed only authors with posts).
2026-11-01
Media: adaptive streaming (HLS) versions of videos
Management API
- addedEvery media asset returns streaming (none | processing | partial | ready): the state of the video's adaptive-streaming (HLS) version. Videos are transcoded when first placed somewhere that plays them (page module, post, collection field, deliverable unit primary file); attachments and other download-only uses are not. The lowest quality becomes playable first (partial), the rest follow (ready). Streaming versions count toward storage (billableBytes) and are removed with the video, never separately (ADR 0008).
- addedmedia.update accepts streaming: true to queue the HLS version of a video right away instead of waiting for the hourly check. VALIDATION when the file is not a video; a no-op when the current version is already transcoded.
- changedUploads accept MOV (video/quicktime) for blog posts and protected files as well as page modules: the original is kept and playback uses the transcoded HLS version, so HEVC/HDR phone recordings play in every browser once transcoded.
- addedCustomer API POST /units/:id/open keeps its shape and adds renditions (original, hls, thumbnails, poster), poster, thumbnails and streaming (partial | ready); url stays the original file's signed URL. hls is a signed segment path served by the site's edge; expiresIn now follows the video length (15 minutes to 4 hours) instead of a fixed 600 seconds, so long lessons no longer cut out mid-way. Kanorio Elements (<kanorio-unit>) and the customer center play hls when present.
- addedmedia.get returns the streaming sources of public videos in renditions: hls (adaptive-streaming master manifest, application/vnd.apple.mpegurl) and thumbnails (progress-bar storyboard VTT) next to original and poster. The Delivery API assets map returns the same two entries as assets[].renditions plus assets[].streaming (partial | ready) for transcoded public videos; videos without a streaming version keep playing url.
2026-10-31
Contacts: contacts.update replaces contacts.update_tags_note and can set the name
Management API
- addedcontacts.update (customers:manage; PATCH /sites/:id/contacts/:contactId) updates a contact's name, tags and/or note. The name is what certificates print and emails use as the greeting; an empty string clears it. Email and phone stay read-only.
- deprecatedcontacts.update_tags_note is deprecated in favour of contacts.update (sunset 2027-04-30). It keeps working with the same input and now also accepts name; its response includes name.
2026-10-31
Deliverables: certificates are rendered on download; preview and branding
Management API
- changedCertificate PDFs are no longer stored at issue time. Customer API GET /deliverables/:id/certificate still returns { serial, issuedAt, url, expiresIn }, but the URL now renders the PDF on request from the current certificate settings, course title, learner name and site branding, so edits reach every certificate already issued. Only the serial and issue date are fixed.
- addedsettings.certificate gains signatureTitle, signatureAssetId (public image asset, printed above the signature line), showLogo (site brand logo at the top, default true) and note (one line under the course title). The frame and heading use the site's brand colour when it is dark enough to read.
- addeddeliverables.certificate_preview (store:view; GET /sites/:id/deliverables/:deliverableId/certificate-preview?learnerName=): short-lived URL of a watermarked sample certificate rendered exactly like a learner's. Nothing is issued or stored.
2026-10-31
Import posts from another site
Management API
- addedimports.* (REST /sites/:id/imports): imports.detect identifies the platform of an old site, imports.discover lists its public posts, imports.create starts a background import of a whole site or specific URLs as DRAFT posts and returns an operationId (op_import_job_<batchId>) for operations.get; After parsing the batch always stops at REVIEW; imports.get shows the rows (failures, missing fields, duplicates) and imports.apply writes the confirmed ones; imports.cancel stops an active batch. Images are copied to the media library, categories and tags are reused, posts link to each other with the new paths and original publish dates are kept. Importing never consumes AI credits. The MCP server version is now 4.3.0.
- addedoperations.get accepts import_job operations; a batch waiting for confirmation is reported as RUNNING with stage=review and result.batchStatus=REVIEW.
2026-10-31
Media: visibility is a file property
Management API
- addedEvery media asset returns visibility (public or protected) and tags. media.list accepts visibility to list only public or only protected files. Protected files are listed next to public ones with the same kinds (image, video, audio, pdf, document); whether a file is protected no longer changes its kind.
- changedUploading, replacing and confirming protected files (media.upload / media.request_upload / media.confirm_upload / media.replace with visibility=protected, media.create_upload_link with target=protected) now needs only the media:manage scope and permission; commerce:manage is no longer required. Attaching a protected file to a deliverable unit (deliverable_units.*) and granting entitlements still need commerce:manage. The MCP server version is now 4.2.0.
- deprecatedAsset kind product is gone: a protected file is reported with its real kind and visibility=protected. media.list still accepts assetKind=product as an alias of visibility=protected until the next major version.
- changedDeliverable units: fileUrl now only ever holds Kanorio template sample media. Units that used to fall back to a raw r2:// URL now reference a media-library asset through primaryAssetId.
- addedTags: media.update accepts tags (any file; replaces the whole list, up to 20 tags of 1-40 characters) and media.list filters with tags (comma-separated, all-of, case-insensitive). Every asset returns tags.
- addedmedia.get returns renditions for public files: an array of { kind: original | mp4 | hls | poster | thumbnails, url, mimeType, width, height, bytes } (original and poster today; HLS and MP4 arrive with transcoding). Protected files return null here and keep being served to buyers through the Customer API.
- changedProtected PDFs now get a first-page thumbnail (rendered server-side, stored privately) and protected videos keep the poster captured at upload. In the API, thumbnailUrl stays null for protected files; their posters will be served through signed playback renditions in a later release. Public PDFs uploaded through the API (no browser) also get a server-rendered thumbnail; media.get shows processing while it is being made.
- addedProtected images: media.upload, media.request_upload and media.create_upload_link (target=protected) accept JPG, PNG and WebP with visibility=protected (pass fileName). They are stored as private files with a private thumbnail, listed with kind image and visibility protected, and can be attached to deliverable units; they get no public variants, are not offered in page or post pickers and cannot be deliverable covers. media.update refuses focus and altText for them (IMAGES_ONLY applies to public images) and visibility cannot be switched for images in either direction (VISIBILITY_BLOCKED, reason unsupported_kind).
- addedVisibility switch: media.update accepts visibility (public or protected) for videos, audio, PDFs and documents (images later). The file is moved between storage buckets in the background: the asset shows processing until media.asset.ready fires. Refused with VISIBILITY_BLOCKED while site content uses a public file (details.reason in_use, with locations) or a deliverable unit uses a protected file (deliverable); other reasons: trash, missing, processing, unsupported_kind, older_version. Old public URLs may stay cached at the CDN edge for a while after the move.
- addedProcessing status: every asset returns processing (null when ready, otherwise { status: pending | processing | failed, kind, error, updatedAt }) and Delivery assets[] gains processing (ready | processing | failed). New webhooks media.asset.ready and media.asset.failed fire when background processing of a file (thumbnails, visibility moves, transcoding) finishes or fails, so you no longer need to poll media.get.
2026-10-28
Site search for custom collections
Management API
- addedCollections accept an optional search setting ({ fields: [text/rich_text fieldIds], pathTemplate?: "/recipes/{slug}" }) on collections.create and collections.update, and return it on every collection. When set, published items of that collection are indexed for site search: GET /sites/:id/search (site.search) and Delivery GET /api/v1/search return them as kind=COLLECTION_ITEM with collectionKey, refId (the item id) and slug. Results always identify the entity first; pathTemplate is only a presentation setting.
- changedSearch hit url is now nullable: it is null for custom-collection items whose collection has no search.pathTemplate (route by collectionKey + slug or fetch the item from the Delivery API). Pages, blog, products and categories keep a non-null url.
2026-10-28
Reviews: moderate, reply, import and settings
Management API
- addedreviews.get, reviews.reject, reviews.reply, reviews.import, reviews.delete_imported, reviews.settings.get and reviews.settings.update. A review is addressed by targetKind (PRODUCT or SERVICE) and targetId; buyer reviews can only be approved or rejected, imported reviews can also be deleted. reviews.reply posts the store's public reply (shown on the storefront and in Delivery GET /reviews). reviews.settings.* read and change per-target settings (enabled, visibility threshold, historical rating) and return the current aggregate. reviews.list adds cursor pagination plus targetTitle, reply, reviewDate and publishedAt on each item; reviews.approve now returns the full review. REST: POST /sites/:id/reviews (import), GET|DELETE /sites/:id/reviews/:reviewId, POST …/reject, PUT …/reply, GET|PATCH /sites/:id/reviews/settings?targetKind=&targetId=. New webhook reviews.replied. The MCP server version is now 4.1.0.
2026-10-28
Service account tokens (knr_sa_)
Management API
- addedService accounts: a website can create non-interactive principals that own their own tokens (prefix knr_sa_). A service account belongs to the website, not to a person, so it keeps working when the person who created it leaves the team; revoking it revokes all of its tokens at once. Effective permissions are token scopes ∩ the service account's grant. Audit entries record actorType SERVICE_ACCOUNT with the service account id as principal.
- changedEach capability now declares which principals may call it (see the capability contracts matrix). Calling a capability that is not available to service accounts returns FORBIDDEN with details.reason = principal_not_allowed. Collections, items, media uploads, entitlements, forms and operations.get accept service accounts; account-level capabilities (sites.create, sites.list, auth.*) and capabilities whose domain services still require a human user remain user-only for now.
2026-10-28
Product publish state and SEO
Management API
- addedproducts.list, products.get and products.update responses include seo (title, description, keywords, Open Graph, canonical URL, robots) and status. products.list accepts status DRAFT. products.create makes a DRAFT product and no longer returns PAGE_LIMIT_REACHED. products.create and products.update accept detailMarkdown, faq, and partial SEO fields (omitted fields stay unchanged; null clears a text field or the share image via ogImageAssetId). Responses add seo.ogImageAssetId and seo.excludeFromSiteSearch. products.publish and products.unpublish (POST /sites/:id/products/:productId/publish and /unpublish, publish rate limit) set ACTIVE or DRAFT. Sample and archived products return PRODUCT_NOT_LISTABLE. Compatible addition on beta capabilities. reviews.list and reviews.approve read and approve reviews addressed by target kind and id.
- changedBreaking (beta): pages.list, pages.get, pages.update and pages.replace_sections no longer expose per-product pages (pageType product). Product copy, SEO and publish state are on products.*. pages.list and pages.get add systemManaged and lockedCoreSectionTypes. pages.create rejects system page types and link-in-bio with SYSTEM_PAGE_LOCKED. pages.delete of a system page returns SYSTEM_PAGE_LOCKED. pages.replace_sections returns CORE_SECTION_VIOLATION when a locked core module is removed, duplicated, or has its source changed. Included in MCP server 4.0.0.
2026-10-27
Headless mode requires approval
Management API
- changedBreaking (beta): site.update_developer_settings rejects frontendMode HEADLESS, and rejects externalStorefrontUrl when that would switch a HOSTED site to HEADLESS, with HEADLESS_ACCESS_REQUIRED (403) until Kanorio approves headless access for that website. The owner applies in the dashboard; approval unlocks the switch and does not change frontendMode by itself. Sites already in HEADLESS can still update the external URL and switch back to HOSTED. The MCP server version is now 4.0.0.
2026-10-27
BUSY (503) admission errors
Management API
- addedAny capability may now fail with BUSY (HTTP 503, Retry-After and details.retryAfter) when the platform is at capacity for that kind of operation (per-process budget or per-site in-flight quota). This is distinct from RATE_LIMITED (429): BUSY is transient load shedding, not a quota you consumed; retry with backoff. Payment confirmations are never shed in favour of other work.
2026-10-27
operations.get, Idempotency-Key, optimistic concurrency on pages.update
Management API
- addedoperations.get (GET /operations/:id) presents every asynchronous business operation with one status model (PENDING / RUNNING / SUCCEEDED / FAILED / CANCELLED / UNKNOWN / EXPIRED), pendingEffects for side effects still in flight, pollAfterSeconds and retention semantics. sites.create mode=brief now returns operationId alongside jobId.
- addedIdempotency keys: capabilities marked x-kanorio-idempotent (items.create, entitlements.grant) accept an Idempotency-Key header on REST and an idempotencyKey argument on MCP. Retrying with the same key and body returns the original result; the same key with a different body is rejected with IDEMPOTENCY_CONFLICT (409); a concurrent in-flight request returns IDEMPOTENCY_IN_PROGRESS (409, Retry-After). Keys expire after 24 hours.
- addedpages.update accepts ifUnmodifiedSince (the page's updatedAt from pages.get) and fails with CONFLICT instead of silently overwriting a page changed by someone else. The version check is applied atomically in the write.
- deprecatedjobs.get is deprecated in favour of operations.get (sunset 2027-04-27). sites.create's jobId field is kept as an alias until then.
2026-10-27
analytics.overview: dataCoverage for traffic sources
Management API
- addedanalytics.overview returns an optional dataCoverage object (trafficSourcesCompleteFrom, note) when the selected range starts before 2026-10-17. Before that date the external referrer was not captured, so direct is overstated in topSources and topUtm for the earlier part of the range.
2026-10-27
entitlements.grant: bring-your-own payments (source EXTERNAL), idempotency keys
Management API
- addedentitlements.grant accepts optional source (MANUAL_GRANT, GIFT or EXTERNAL), sourceRef and idempotencyKey. Use source EXTERNAL with your own transaction id as sourceRef when the payment was collected outside Kanorio: retries with the same sourceRef or idempotencyKey return the same entitlement, a revoked external grant is never re-created by a replay, and the granted webhook fires only once. EXTERNAL is accepted from agent tokens (PAT / OAuth) only. Kanorio Checkout does not need to be enabled.
- addedEntitlement.source may now be EXTERNAL or GIFT in entitlements.list / check responses and in deliverables.entitlement.* webhook payloads.
- changedHosted checkout line items accept the generic shape { kind, refId, quantity, options } in addition to { productId, tierId, quantity }; kind defaults to PRODUCT. Other kinds are rejected with LINE_KIND_NOT_SUPPORTED until they are enabled.
2026-10-26
Header navigation overflow and a two-row layout
Management API
- addedHeaderSection.layout accepts "top-stacked": navigation renders on its own full-width row below the logo/CTA row, for sites with many nav items.
- changedThe rendered site header now collapses navItems that do not fit the available width into a "more" menu instead of silently clipping them, at any content amount or screen size.
- addedglobals.replace_header and globals.create_header responses include an optional warnings array (code NAV_LABEL_TOO_LONG or NAV_TOO_MANY_ITEMS) flagging navItem content issues; this does not predict layout overflow, since the renderer always collapses into "more".
2026-10-26
Delivery API: per-site rate limiting first, hard query timeout
Management API
- changedContent Delivery API origin rate limits are now evaluated per site before the platform-wide bucket. A single site exhausting its quota no longer causes 429 responses for other sites. 429 responses carry { error, code: "RATE_LIMITED" }, Retry-After and Cache-Control: no-store.
- addedGET /api/v1/collections/{key}/items and items.list may return 503 QUERY_TIMEOUT (SERVICE_UNAVAILABLE on the Management API) when a filter/sort combination exceeds the 2-second statement timeout. Narrow the filter or retry; the response is never cached. Pass total=false when you only paginate by cursor to skip the COUNT(*) query.
2026-10-26
Listing pages are only indexed when they exist and have content
Management API
- addedsite.update_developer_settings (PATCH /v1/sites/:id/developer-settings) accepts and returns externalListings: the Kanorio-managed listing pages (blogIndex, blogCategory, blogTag, blogAuthor, shopIndex, productCategory) a HEADLESS site's external frontend implements. Only declared listings appear in sitemap.xml, IndexNow notifications and llms.txt; none are declared by default.
- addedContent Delivery API GET /api/v1/site returns listings, the state of each listing page: live (index it), empty (served but nothing to list, render with robots noindex) or off (not served). @kanorio/sdk adds seo.listingRobots(site, key).
- changedsitemap.xml, IndexNow notifications, llms.txt and site search no longer include the shop home or a category page when there is nothing to list (no products in the public catalog, or an empty category); the hosted /blog and /shop pages render with robots noindex until the first post or product is published. HEADLESS sites stop listing undeclared listing pages.
2026-10-26
Search Console capabilities, DNS verification for Kanorio-managed domains
Management API
- addedseo.search_console.status (GET /sites/:id/seo/search-console, scope seo:view) reports the Search Console connection, the property and sitemap Kanorio uses, ownership verification and its source (meta, existing, dns), the last sitemap submission, and a pending re-verification with its lastFailureCode.
- addedseo.search_console.verify_and_submit (POST /sites/:id/seo/search-console/verify, scope seo:manage) verifies ownership, adds the property and submits the sitemap, same as the dashboard's Check again. Connecting the Google account remains a browser OAuth step for the owner. Errors: NO_PUBLIC_URL, SEARCH_CONSOLE_NOT_CONNECTED, META_NOT_LIVE, OWNERSHIP_REQUIRED, SITEMAP_NOT_REACHABLE, DNS_NOT_PROPAGATED, SEARCH_CONSOLE_* (all 422/429/502/503, never 403).
- addedScopes seo:view and seo:manage can now be granted to agent tokens and OAuth clients.
- changedHEADLESS sites whose externalStorefrontUrl host is under a domain with Kanorio-managed DNS (bought through Kanorio, DNS mode KANORIO) are now verified automatically: when the Google account owns no covering property, Kanorio writes a google-site-verification TXT record to the zone and verifies via DNS. Until Google sees the record the flow returns DNS_NOT_PROPAGATED and a background job retries; the record is removed on disconnect.
2026-10-26
IndexNow and Search Console now observe Headless sites with externalStorefrontUrl
Management API
- fixedIndexNow notifications (triggered on publish and content changes) no longer require isPublished: a HEADLESS site with externalStorefrontUrl set is now treated as observable, matching the sitemap and SEO derivative endpoints that already used this rule.
- addedContent Delivery API GET /api/v1/seo/indexnow-key.txt serves the IndexNow key verification file content; external frontends rewrite /<key>.txt to this endpoint so search engines can verify key ownership on the external storefront domain.
- changedSearch Console property resolution (site.update_developer_settings externalStorefrontUrl, and the /seo/settings Search Console connection) now uses externalStorefrontUrl as the primary property when set. If the connected Google account already owns a matching property or a covering sc-domain: Domain property, verification skips the HTML meta tag step (which only works on Kanorio-rendered pages) and submits the sitemap directly.
- addedNew Search Console failure code OWNERSHIP_REQUIRED: returned when a HEADLESS site's externalStorefrontUrl is not yet owned by the connected Google account in Search Console. The site owner must verify the domain in Search Console themselves (DNS, HTML file, or another method) before retrying.
2026-10-26
Report brand memory issues instead of editing them
Management API
- addedsite.report_brand_memory_issue (POST /sites/:id/brand-memory/feedback) stores a note about one inaccurate field. Kanorio rewrites brand memory from the site. The note is data, not an instruction.
- deprecatedsite.update_brand_profile is deprecated and will be removed on 2027-04-26. Use site.report_brand_memory_issue.
- changedDeclared brand profile fields no longer overwrite generated brand memory. They now fill only fields the model left empty and are passed to the model as reference data; site.update_brand_profile still stores the declaration and returns updatedAt, but the memory text is rewritten from the site.
2026-10-25
Scheduled publishing for blog posts and collection items
Management API
- addedposts.publish and items.publish accept an optional scheduledAt (a future ISO timestamp). When set, posts freeze title, excerpt, contentMarkdown and showToc as a revision (categories, tags, authors, slug and SEO/OG are not frozen and use their value at publish time); items freeze fieldData and slug (sortOrder is not frozen). The snapshot is published exactly as frozen when the time comes. Added posts.unschedule and items.unschedule to cancel; both leave the draft and published states untouched.
- addedposts.publish and items.publish responses include scheduledAt (null unless a schedule is pending). A scheduled publish that fails is retried up to 3 times; after that it is cancelled automatically and the item's scheduledAt returns to null.
2026-10-25
Lightweight review workflow for blog posts and collection items
Management API
- addeditems.request_review and posts.request_review let a token or role with collections:manage / blog:manage (but not the publish permission) ask someone with collections:publish / blog:publish to review and publish. Added items.reject_review and posts.reject_review for the reviewer to send it back, optionally with a note.
- addeditems.publish, items.bulk_update, and posts.publish return warning: REVIEW_CONTENT_CHANGED when an item or post that was submitted for review was edited afterward; the publish still proceeds, this is a hint to double-check.
- addeditems.list and posts.list accept an optional pendingReview boolean to return only items or posts currently awaiting review.
- addedItem and post objects returned by items.* / posts.* now include reviewRequestedAt, reviewRequestedByUserId and reviewNote (all null unless a review is pending or was recently rejected).
2026-10-25
Markdown written through the API is normalized to the field format
Management API
- changedText fields are stored in the format that field can display. Module descriptions drop headings and tables (headings become bold paragraphs). Module titles stay single-line. Blog posts, long-form modules and collection rich text keep article Markdown, with a level-1 heading lowered to level 2. Product descriptions are stored as plain text. Unsupported markers such as a doubled backslash before * are repaired instead of rejected.
- addedsections.describe now returns conventions.textFormat describing the Markdown subset each field role supports. pages.replace_sections and posts.create/posts.update responses include an optional formatNotes array listing fields whose submitted text was reformatted to match the field's supported syntax.
2026-10-25
Version history, trash and optimistic concurrency for custom collection items and blog posts
Management API
- addedCustom collection items now have the same 30-day version history as website pages and blog posts (revisions.list / revisions.get / revisions.restore accept subject: CONTENT_ITEM, subjectId: itemId). Items keep up to 50 revisions each.
- addeditems.delete now moves an item to the trash (soft delete) instead of deleting it immediately; it keeps counting against the plan's item quota while trashed. Added items.list_archived to list trashed items, items.restore to bring one back, and items.purge to permanently delete a trashed item and free its quota immediately.
- addeditems.update and items.bulk_update accept an optional ifUnmodifiedSince (the item's updatedAt from items.get/items.list); a mismatch returns CONFLICT with the current updatedAt instead of silently overwriting a change made by someone else or another agent run since you last read it.
- changedItem objects returned by items.* now include archivedAt (null unless the item is in the trash).
- addedposts.update accepts an optional ifUnmodifiedSince (the post's updatedAt from posts.get/posts.list); a mismatch returns CONFLICT instead of silently overwriting a change made by someone else or another agent run since you last read it.
2026-10-24
Fine-grained scopes for publishing, domains, developer settings, media, orders and collection schemas
Management API
- changedCapabilities now require the same fine-grained permissions as the dashboard: site.publish requires site:publish (was site:lifecycle); domains.status and domains.bind require site:domains; site.update_allowed_origins, site.update_developer_settings and site.create_preview_token require developer:manage; collections.create / update / delete require collections:schema; orders.list and orders.get require orders:view; media reads require media:view and media writes (upload, request_upload, confirm_upload, create_upload_link, import_stock, replace, update, trash, restore, purge) require media:manage; campaigns.list requires marketing:view and campaigns.create_draft / update_draft require marketing:manage.
- changedExisting tokens are upgraded so they keep exactly what they could do before (for example, tokens with site:lifecycle gain site:publish, tokens with store:view gain orders:view, and tokens with content:edit gain media:manage). The site owner's team role still applies: effective permissions are token scopes intersected with your role on each website.
- changedThe content preset now includes collections:manage and media:manage, and the view preset includes orders:view and media:view. marketing:send is no longer offered to agents because sending campaigns stays in the dashboard.
- addedauth.whoami accepts an optional websiteId and then returns site { role, roleName, memberPermissions, effectivePermissions, blockedByRole } so an agent can tell a missing token scope apart from a team role that does not allow the action.
- addedThe OAuth consent screen lets users change the requested permissions (view only, edit content, full access, or a custom set) before approving; the token response scope reflects what was granted. After approval, users can narrow a connected app's permissions from Account → API access → Connected apps, which applies immediately to all of that app's access and refresh tokens; adding permissions back requires the app to re-authorize.
2026-10-24
billableBytes, edgeColor, ogImageFraming, imageFramings, and 20 GB storage add-on levels
Management API
- addedMedia asset objects returned by media.* and content capabilities now include billableBytes (integer): the bytes counted toward the site's storage quota, including the 256 KB per-image minimum and active derived files such as watermarked versions.
- addedMedia asset objects returned by media.* and Delivery API assets maps now include edgeColor (string | null): the #rrggbb border color when all four edges of an image share a uniform opaque background, "transparent" for cutout images, or null for multi-colored images.
- addedpages.update and posts.create / posts.update accept optional ogImageFraming ({ fitMode?: "contain" | "cover", padColor?: "#rrggbb", focus?, screens? }), and products.create / products.update accept optional imageFramings (parallel to images) to control share-image and product-image framing without cropping the original asset.
2026-10-23
Per-device image aspect ratio and image framing in sections
Management API
- changedThe imageAspect field of image-text-split sections now also accepts any W:H ratio between 1:4 and 5:1 (for example "4:5") and a per-device object { desktop, tablet, mobile } whose values are "natural" or a W:H ratio. The existing values natural, 16:9, 4:3, 1:1 and 3:4 keep their meaning; sections read back may contain the object form.
- addedpage-main-visual and full-width-image sections may carry an imageRatio field, and image fields may carry a sibling …Framing field (focus point and per-device crop) that site owners set in the editor. Both are presentation settings: sections.describe does not list them, and they are preserved when you write a section back unchanged.
2026-10-23
AI bot page breakdown, per-page AI assistant referrals, and analytics query rate-limit tier
Management API
- addedanalytics.bots now accepts optional breakdown ('none' | 'page' | 'page_bot'), pageRef, botToken, limit (1-500, default 100), and cursor parameters. breakdown='page' returns per-page hits by intent (retrievalHits, trainingHits, searchHits, blockedHits, spoofedHits) across up to 3650 days; breakdown='page_bot' returns per-page × per-bot rollups retained for the last 90 days.
- addedanalytics.bots breakdown responses include nextCursor (the next offset, or null on the last page); cursor accepts 0-50000. An unrecognised botToken returns VALIDATION with details.reason UNKNOWN_BOT, and a breakdown='page_bot' range that starts before the 90-day retention window returns VALIDATION with details.reason RANGE_BEFORE_RETENTION and details.earliest.
- addedanalytics.* return ANALYTICS_BUSY (HTTP 429 with Retry-After) when the analytics backend is saturated or a query exceeds its time limit; retry after details.retryAfter seconds or narrow the date range.
- addedanalytics.pages now includes aiReferralEntries on each page item, counting entry sessions attributed to AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot).
- changedThe offset parameter of media.list, products.list, orders.list, items.list, and posts.list now accepts 0-50000; larger values return VALIDATION. Narrow the query with filters instead of paging past 50,000 rows.
- changedanalytics.overview, analytics.pages, analytics.bots, and analytics.edge now use the agentHeavy rate-limit tier: each capability allows 20 requests/minute per token shared across MCP and REST, plus 20 requests/minute per site across all tokens. Exceeding either returns RATE_LIMITED with Retry-After.
2026-10-23
Activity log redacts internal fields of platform actions
Management API
- securitysites.activity now returns only the documented fields in summary for actions performed by Kanorio staff (for example category and reason for admin.takedown); internal notes and bypassed-lock details are no longer included.
2026-10-23
Protected files for deliverables, file uploads and large-file uploads
Management API
- addedmedia.upload now accepts videos, audio, PDFs, documents, e-books and ZIP files (pass fileName; up to 40 MB as base64 or 100 MB by url) and an optional visibility: "public" (default) or "protected" for deliverable files stored privately and only served to buyers through signed short-lived links (needs commerce:manage).
- addedmedia.request_upload (POST /sites/:id/media/uploads) and media.confirm_upload (POST /sites/:id/media/uploads/confirm) upload files up to 5 GB: request a one-time PUT URL, send the file, then confirm.
- changedmedia.replace now also replaces videos, audio, PDFs, documents and protected files (pass fileName; same kind only, KIND_MISMATCH otherwise) instead of returning IMAGES_ONLY.
- addedmedia.create_upload_link accepts target: "images" (default), "files" or "protected", and media.upload_link_status returns target; media.get usages include deliverable covers and unit files (kind "deliverable").
- changedBreaking (beta): deliverable_units.replace_tree and deliverable_units.update now reject public files as primaryAssetId / attachmentAssetIds with ASSET_NOT_ALLOWED, and deliverables.create / deliverables.update reject a coverAssetId that is not an image. Files in the trash or older versions return ASSET_NOT_FOUND. Opening a unit that still points to a public file returns FILE_NOT_PROTECTED (409).
- changedmedia.trash and media.purge now accept protected files that no deliverable uses; PRODUCT_FILE_PROTECTED is returned only while a deliverable uses the file.
- addedmedia.update (PATCH /sites/:id/media/:assetId) sets an image's default focus point, default alt text and watermark exclusion; asset objects now include focus, altText and watermarkExempt. Content Delivery API assets[] add focus, and alt falls back to the asset's default alt text.
- changedThe MCP server version is now 3.0.0 (initialize serverInfo and server.json).
2026-10-23
Analytics custom date ranges, daily series & period comparison, storage quota accuracy, dynamic MCP tools/list, and coverage expansion
Management API
- fixedanalytics.overview, analytics.pages, and analytics.bots now compute day boundaries in the website's IANA timezone, accept custom from/to (YYYY-MM-DD) ranges up to 3650 days, backfill unaggregated or pre-migration days from raw events, and deduplicate device and country counts by unique visitor.
- addedanalytics.overview now returns daily series, previousPeriod totals, percentage trends, timezone, and dataStatus; analytics.pages now includes cross-tool identifiers (websitePageId, productId, postId); analytics.edge exposes Cloudflare CDN & WAF edge metrics.
- fixedmedia.list usage now returns usedBytes, limitBytes, and remainingBytes (with quotaBytes set to the plan storage limit in bytes) and excludes broken missing-file records by default unless includeMissing=true.
- changedMCP tools/list now filters tools by the token's granted scopes and active feature flags, and read-only capabilities site.get_settings and domains.list now require console:view instead of site:manage_settings. FORBIDDEN errors now include structured details.reason and details.resolution.
- addedAdded auth.whoami, sites.usage, sites.diagnose, site.activity, store.overview, promotions.list, submissions.update_status, blog_categories.create, and blog_tags.create.
- addedContent Delivery API OpenAPI specification (packages/shared/openapi/delivery.json) now documents collection query parameters (filter bracket notation with 15 operators, 2-level any OR groups, relative date expressions now[+-]Nunit, sort, total=false, and locale), X-RateLimit-* headers, and technical guardrails; all /developers/* pages now serve canonical .md Markdown counterparts.
2026-10-22
Website frontendMode (HOSTED | HEADLESS) as single source of truth
Management API
- addedsite.update_developer_settings (PATCH /v1/sites/:id/developer-settings) now accepts and returns frontendMode ("HOSTED" | "HEADLESS"). Setting frontendMode to "HEADLESS" enables service availability (Content Delivery API, form submissions, blog publishing, customer sign-in) without publishing the hosted site and switches the backoffice navigation to the backend-only surface.
- deprecatedheadlessEnabled on site.update_developer_settings is deprecated in favor of frontendMode (passing headlessEnabled: true maps to frontendMode: "HEADLESS", and headlessEnabled: false maps to "HOSTED").
- addedContent Delivery API GET /api/v1/site now includes frontendMode ("HOSTED" | "HEADLESS") in the site metadata response.
- addedsite.update_brand_profile (PATCH /v1/sites/:id/brand-profile) lets agents declare brand positioning, targetAudience, toneAndStyle, keyOfferings, and valueProposition directly into Website.brandProfile and Website.aiMemory.
- addedHosted-only mutation capabilities (pages.create/update/reorder/delete/replace_sections, site.publish/unpublish, globals.create_header/delete_header/replace_header/replace_footer) now return 409 HOSTED_ONLY when called against a website in HEADLESS frontendMode.
2026-10-21
Headless brochure sites and blog: system host, portable content, reference slug filters and developer settings
Management API
- addedsite.check_external_frontend (GET /v1/sites/:id/external-frontend/check, CLI kanorio doctor --site): probe externalStorefrontUrl for SEO/AEO derivative rewrites (sitemap.xml, robots.txt with AI crawler rules, llms.txt, feed.xml), JSON-LD @graph, published page content sync, 24h pageview reporting, and active webhook revalidation.
- addedContent Delivery API GET /api/v1/seo/{llms.txt,llms-full.txt,sitemap.xml,robots.txt,jsonld?path=} and POST /api/v1/analytics/pageview for external storefronts, plus @kanorio/sdk seo.rewrites({ framework }), seo.jsonLd(path), revalidate.handler() and <KanorioAnalytics />.
- addedCollection query DSL: match operator (field-scoped, multi-term AND substring match on text and rich_text fields; use GET /api/v1/search for site-wide keyword search), any 2-level OR groups, includeDepth=1, locale validation (UNSUPPORTED_LOCALE), and optional total=false.
- addedcollections.create and collections.update accept accessPolicy ({ gatedFields, requires: { kind: "customer" } | { kind: "entitlement", featureKey }, itemOverrideField? }) to gate specific collection or blog fields behind customer sign-in or a commercial entitlement without coupling blog and membership code. Anonymous Content Delivery API requests strip gatedFields and return a top-level access summary (still edge-cached); requests bearing a Customer Access Token (Authorization: Bearer knr_cat_…) evaluate entitlements.check and return private, no-store.
- addedDeliverables accept grants: string[] (commercial feature keys unlocked on purchase or grant), and entitlements.grant / entitlements.check / entitlements.list accept featureKey (e.g. "premium").
- addedContent Delivery API GET /api/v1/feed (RSS 2.0) and GET /api/v1/feed.atom (Atom 1.0) with ?tag=, ?author= and ?limit= filters, respecting collection accessPolicy; blog items now expose derived primary_author and primary_category fields (expandable via ?expand=, not filterable) and callout segments.
- changedContent Delivery API: Authorization: Bearer knr_pat_… / knr_at_… / knr_pv_… without ?preview=1 now returns 401 UNAUTHORIZED instead of being silently treated as anonymous; management and preview tokens must never reach a browser. An invalid or expired knr_cat_ token also returns 401 (WWW-Authenticate: Bearer error="invalid_token") instead of degrading to the anonymous response.
- changedCustomer Access Tokens and hosted customer sessions remain valid on sites that are headlessEnabled but not published (service availability instead of render availability), so the Customer API works for headless sites that never publish the hosted template.
- changedHosted blog pages, /rss.xml, llms-full.txt and the public blog read path honor the blog collection accessPolicy: anonymous visitors get the excerpt and a sign-in prompt; signed-in customers unlock through the same Delivery API endpoint.
- addedsite.create_preview_token (POST /v1/sites/:id/preview-tokens): mint a read-only knr_pv_ preview token for reading unpublished drafts via the Content Delivery API (send X-Kanorio-Preview: knr_pv_… or Authorization: Bearer knr_pv_… with ?preview=1).
- addedsite.update_developer_settings (PATCH /v1/sites/:id/developer-settings): enable headlessEnabled (Content Delivery API, form submissions and blog publishing work while the hosted site remains unpublished) and set externalStorefrontUrl (hosted pages 302-redirect to your external frontend while /api/v1/*, /checkout and /account stay active); returns the site's permanent systemHost (s-<base32>.kanorio.com).
- addedposts.get, posts.create, posts.update, posts.publish and posts.unpublish now include structured segments[] and toc[] (H2/H3 headings with anchor ids) alongside contentMarkdown.
- addedFilter operators eq_slug (reference fields) and has_slug (multi_reference fields) in items.list and the Content Delivery API: filter by a referenced item's slug without a preliminary id lookup (for example ?filter[categories][has_slug]=news).
- addedContent Delivery API /api/v1/collections/{key}/items and .../{slug} now return a top-level assets map (url, width, height, alt, mimeType, fileSize, kind), full published fieldData on expand, X-Kanorio-Cache-Tags headers, and portable blog content with ?formats=markdown,html,plaintext.
- addedglobals.list_headers, globals.create_header and globals.delete_header (GET|POST /v1/sites/:id/globals/headers, DELETE …/:key), plus key parameter on globals.replace_header: manage multiple site headers for sub-brands or campaign pages.
- addedwebhooks.list, webhooks.create, webhooks.update_events, webhooks.delete and webhooks.test (under /v1/sites/:id/webhooks, scope site:integrations): register and manage webhook endpoints programmatically (up to 10 endpoints per site on all plans, including collectionKeys and formIds filters).
- changedsite.publish, posts.publish, pages.update and products.create are now available on all plans without page-count, blog-publishing, product-page visibility or product-count restrictions (FREE_VISIBLE_PAGE_LIMIT, PRODUCT_PAGES_PLAN_REQUIRED and PRODUCT_LIMIT_REACHED removed; posts.publish no longer returns PRO_REQUIRED). pages.create and products.create enforce a platform-wide technical safety cap of 1,000 pages per website (PAGE_LIMIT_REACHED, HTTP 422).
2026-10-21
Deliverables: courses, e-books and downloads as a first-class resource
Management API
- addeddeliverables.list/get/create/update/archive (GET|POST /v1/sites/:id/deliverables, GET|PATCH /v1/sites/:id/deliverables/:deliverableId, POST …/archive): a deliverable is what a buyer receives (course, e-book, audio series, template pack) — chapters and units, completion rule, certificate and comment settings. Products only sell it.
- addeddeliverable_units.replace_tree (PUT /v1/sites/:id/deliverables/:deliverableId/units), deliverable_units.update (PATCH /v1/sites/:id/deliverable-units/:unitId) and deliverable_units.reorder: two-level chapter/unit tree; unit kind is an open string (video, audio, file, text, quiz); files are referenced by media-library assetId; isPreview, releaseAt/releaseAfterDays, allowDownload and tier scope per unit.
- addedentitlements.list/grant/revoke/check (GET|POST /v1/sites/:id/entitlements, POST …/:entitlementId/revoke, GET …/check): who can access which deliverable, grants without a purchase (idempotent), and a point-in-time access check that accounts for preview, release schedule and expiry.
- addedprogress.list (GET /v1/sites/:id/deliverables/:deliverableId/progress): learner progress per deliverable.
- addedproducts.update accepts deliverables: [{ deliverableId, tierId?, accessDays? }] to replace the deliverables granted on purchase; products.* responses include deliverables.
- addedWebhook events deliverables.entitlement.granted, deliverables.entitlement.revoked and deliverables.progress.completed (identifiers only, no personal data).
- changedDigital products no longer carry content directly: existing product contents were migrated into one deliverable per product and linked back automatically. Access checks now evaluate entitlements against deliverables and units.
2026-10-19
Nested pages, automatic 301 redirects and breadcrumbs
Management API
- addedpages.list, pages.get, pages.create and pages.update now return PagePathInfo fields: path (string[]), parentSlug, isDirectory, depth (0-3), ancestors, breadcrumbs and breadcrumbSource ("url" | "custom").
- addedpages.create and pages.update accept optional parentSlug (pass parentSlug plus the last segment rather than concatenating multi-segment paths) and breadcrumbParentSlug (override the breadcrumb parent without changing the URL; follows the target page when it is renamed and is cleared when it is deleted). Missing parent paths are automatically created as hidden folder pages. Renaming or moving a page automatically updates all descendant subpages and creates 301 redirects for previously published pages (pass createRedirect: false on pages.update to skip). Both return warnings (e.g. REDIRECT_SHADOWS_PAGE) when applicable.
- addedpages.delete accepts mode: "subtree" | "lift-children" when deleting a parent page that has subpages, and createRedirect (defaults to true) to control the 301 redirects created by "lift-children".
- addedglobals.update_site_settings accepts showBreadcrumbs (boolean, defaults to true) to toggle site-wide breadcrumb navigation and BreadcrumbList JSON-LD.
- addedsites.structure (GET /v1/sites/:id/structure): return the visual sitemap tree of a website — top-level pages with nested subpages (up to 3 levels), real page status, the ordered list of section types on each page, and summary counts for Kanorio-managed Store and Blog pages. Requires editor:enter.
2026-10-19
Site search
Management API
- addedsite.search (GET /v1/sites/:id/search?q=&kind=&limit=): search the live site the way visitors do — published pages, blog posts, blog categories and tags, products and product categories — with a snippet around the first matching term. Every term must match (AND); full-width characters, case and term order do not matter. Requires console:view. The same index serves https://{site}/api/v1/search.
- addedpages.update and posts.create / posts.update accept excludeFromSiteSearch (boolean, also returned on pages.list / pages.get / posts.get seo.excludeFromSiteSearch) to hide a published page, product page or blog post from visitor site search without affecting search engine indexing (robotsIndex).
2026-10-19
Header utility buttons
Management API
- addedglobals.replace_header: new optional field utilities, an ordered list of { id, enabled } for the feature buttons shown after the header CTA buttons (search, cart). An omitted utility keeps its default (search off, cart on; newly created sites default both to on), so existing headers render the same; a utility only appears when the site has that feature (search: published site with searchable content; cart: shopping cart turned on with at least one active product).
2026-10-19
Collections: structured content with a public Content Delivery API
Management API
- addedcollections.list/get/create/update/delete and items.list/get/create/update/delete/publish/unpublish/bulk_create/bulk_update (under /v1/sites/:id/collections): define collections with 17 field types plus json, write items with per-field validation errors, and read them anonymously from https://{site}/api/v1/collections/{key}/items with filter/sort/cursor paging, edge caching and CORS allowlists. New scopes collections:view and collections:manage.
- addedsite.get_allowed_origins and site.update_allowed_origins (GET/PUT /v1/sites/:id/allowed-origins): manage the CORS allowlist for the Content Delivery API.
- addedWebhook events collections.item.created/updated/published/unpublished/deleted and collections.items.bulkChanged.
- addedSystem collections blog, blog_categories, blog_tags and blog_authors expose the site's blog through collections.*, items.list/get and the Content Delivery API. They are read-only through items.*: writes return SYSTEM_COLLECTION; use blog.* or the Blog page. Changes made through the blog appear in the collection within the same request.
2026-10-17
Automatic Kanorio branding badge removal on paid plans
Management API
- changedglobals.update_site_settings: siteSettings no longer accepts hidePoweredByKanorio (the key is ignored if sent). The Kanorio branding badge on public sites is now shown on the free plan and hidden automatically on paid plans.
2026-10-17
Content revision history and restore
Management API
- addedrevisions.list, revisions.get and revisions.restore (GET /v1/sites/:id/revisions, GET /v1/sites/:id/revisions/:revisionId, POST /v1/sites/:id/revisions/:revisionId/restore): inspect 30-day revision snapshots for website drafts and blog posts, and restore either a single page or an entire draft with automatic pre-restore backup.
2026-10-17
Header and footer layout, brand slot and social link controls
Management API
- addedglobals.replace_header: new optional fields layout top-left, surface, showBrandLogo, showSiteName, logoSize, showSocialLinks (boolean or { desktop, tablet, mobile }), hiddenSocialLinkUrls, hiddenSocialLinkUrlsByDevice, showAccentLine and ctaIds (up to 2 buttons; ctaId keeps working).
- addedglobals.replace_footer: new optional fields logoFramed, hiddenSocialLinkUrls, ctaIds, ctaStyleById and showAccentLine; navItems accept linkType group, rendered as its own column.
- addedpage-main-visual and link-in-bio sections accept hiddenSocialLinkUrls to hide some of siteSettings.socialLinks in that section.
- deprecatedHeader variant (solid / transparent / glass) is accepted but ignored; the header background is controlled by surface.
2026-10-17
Website analytics read endpoints
Management API
- addedanalytics.overview, analytics.pages, and analytics.bots (GET /v1/sites/:id/analytics/overview, /pages, /bots): read-only traffic totals, attribution sources (including AI assistants and UTM campaigns), per-page engagement metrics, and AI crawler activity.
2026-10-16
Content references media by assetId
Management API
- changedBreaking: pages.get, pages.replace_sections, globals.*, posts.* and campaigns create/update show this site's media as asset:<assetId> inside sections, header, footer, site settings, Markdown and email content, and add an assets map (assetId → asset object) to the response. URLs with a size variant or query, and URLs of other websites, are returned unchanged. Content read this way can be written back as is.
- changedBreaking: content writes accept asset:<assetId> for this site's media and check it: ASSET_NOT_FOUND (details.assetIds) when the file is not in the site's media library, ASSET_NOT_ALLOWED when it is a product file.
- changedBreaking: the share image input ogImage (an https URL) is replaced by ogImageAssetId on pages.update, posts.create and posts.update; null removes the share image. seo.ogImageAssetId is added to page and post responses, and seo.ogImage stays as the display URL.
- changedBreaking: products.create and products.update take images as { assetId, alt? } instead of { url, alt?, width?, height? }; width and height come from the file. Product responses add images[].assetId (null for images that are not in the media library).
- changedThe MCP server version is now 2.0.0 (initialize serverInfo and server.json).
2026-10-16
Media library assets
Management API
- addedmedia.get (GET /v1/sites/:id/media/:assetId): one file from the media library or the trash with every place the site uses it. New error code: ASSET_NOT_FOUND.
- addedmedia.replace (POST /v1/sites/:id/media/:assetId/replace): upload a new version of an image from base64 or an https URL. Every place that uses it switches over and keeps the same assetId; published pages switch after the next publish (awaitingPublish). Images only for now. New error codes: IMAGES_ONLY, REPLACE_NOT_AVAILABLE.
- changedBreaking: media.* return one asset object (assetId, kind, status, missing, version, url, thumbnailUrl, fileName, mimeType, bytes, width, height, durationSeconds, pageCount, source, inUse, createdAt). media.upload and media.import_stock return { asset } instead of { id, url, … } (id was the CDN image id, not the assetId); media.upload_link_status returns asset objects with uploadedAt.
- changedBreaking: media.list lists the whole media library as in the dashboard (images, video, audio, PDFs, documents, product files). Inputs are assetKind, unusedOnly, search, sort and offset (kind and limit were removed); the response adds hasMore and pages hold 40 files.
2026-10-15
Replaced image versions
Management API
- changedReplacing an image keeps the older version until no published page uses it, then moves it to the trash. media.restore on such an older version restores it as a separate file and returns its new assetId; content that used the image keeps showing the current version. New error code: RESTORE_FAILED.
- changedmedia.list leaves out older versions kept after an image is replaced.
2026-10-15
Media trash
Management API
- addedmedia.trash, media.restore and media.purge (POST /v1/sites/:id/media/:assetId/trash, POST …/restore, DELETE /v1/sites/:id/media/:assetId). Trashed files stop showing on the site right away, still count toward storage, and are deleted permanently after 30 days. media.purge only accepts files already in the trash and fails with ASSET_IN_USE (details.usages) unless confirm: true when the site still uses the file. New error codes: NOT_IN_TRASH, PRODUCT_FILE_PROTECTED, EXPORT_HOLD, ASSET_IN_USE, DELETE_FAILED.
- changedmedia.list no longer returns files in the trash; they still count toward usage.quotaBytes.
2026-10-14
OAuth client authentication, scope defaults and stability labels
Management API
- addedOAuth token endpoint accepts private_key_jwt client assertions (RFC 7523) for CIMD and DCR clients that publish a jwks_uri; the authorization server metadata now advertises token_endpoint_auth_methods_supported [none, private_key_jwt].
- changedAuthorization requests without a scope parameter (or with only unknown scopes such as offline_access) no longer fail with invalid_scope; the default "content" permission set is granted and shown on the consent page. Protected resource metadata now lists scopes_supported.
- addedEvery operation carries x-kanorio-stability in the OpenAPI document; deprecated operations are flagged with deprecated: true, x-kanorio-sunset and x-kanorio-replaced-by, and REST responses send Deprecation, Sunset and Link headers. The whole API is currently beta.
- addedMCP step-up authorization: when an OAuth access token lacks a capability's requiredScope, /api/mcp now responds with HTTP 403, WWW-Authenticate: Bearer error="insufficient_scope", scope="..." and _meta["mcp/www_authenticate"] so clients can re-authorize with unioned scopes.
- changedPerplexity is now documented as an API-key client; its OAuth registration expects a client_secret that Kanorio does not issue.
2026-10-05
Management API beta
Management API
- addedREST API at /api/v1 and MCP server at /api/mcp backed by the same capabilities registry; personal access tokens and OAuth 2.1 (PKCE, CIMD, DCR).
- addedOpenAPI 3.1 document at /api/v1/openapi.json and the kanorio CLI.