---
title: "Changelog"
description: "Every change to the Anlyon API contract, with the date it took effect and a migration note."
---

Every change to the API contract is listed here with a migration note. The newest entry is at the top. Dates are UTC.

## 2026-10-05

### A plan counts environments, and every workspace starts with Production

**Breaking, billing responses.** `limits.maxEnvironments` and `betaAllowances.environments` are one number, or the string `"unlimited"`. They were objects keyed by `development`, `staging` and `production`.

Affected responses:

- `GET /api/v2/billing/plan`
- `GET /api/v2/billing/plans`
- `GET /api/v2/public/pricing-policy`

| Plan | Environments |
| --- | --- |
| Early Beta Access | 3 |
| Free | 1 |
| Production | 2 |
| Growth | 3 |
| Enterprise | unlimited |

What else changed with it:

- A new workspace gets a default environment of kind `production`, named Production, with protection on. Existing environments keep their kind, name and slug.
- A workspace creates environments of kind `development`, `staging` and `production` inside one total. `PATCH /api/v2/workspaces/{workspaceId}/environments/{environmentId}` changes `kind` with no plan check.
- A create over the total returns `429` `QUOTA_EXCEEDED`.
- Early Beta Access includes environment protection, promotion between environments and the email notification channel. Email is capped at 500 emails per workspace per UTC month. `features.protectedProduction` and `features.controlledPromotion` are `true` there.
- The message of `PAID_PLANS_UNAVAILABLE` reads "Paid plans arrive after the beta. Nothing was charged. Your workspace keeps its Early Beta allowances." The message of `HTTPS_REQUIRED` reads "HTTPS is required." Codes and statuses are unchanged.

**Migration note.**

- Read `maxEnvironments` and `betaAllowances.environments` as a number or the string `"unlimited"`. A client that read `maxEnvironments.production`, or summed the three keys, reads the one value.
- `@anlyonhq/sdk` types `BillingPlanLimits.maxEnvironments` and `BetaAllowances.environments` as `number | 'unlimited'`. Update the package and fix the type errors it reports.
- Deleting a secret or a schedule in a protected environment needs `confirmProtected: true`. That now applies to the default environment of a new workspace.
- A client that matched on the old message text of `PAID_PLANS_UNAVAILABLE` or `HTTPS_REQUIRED` matches on the code.

### The Stripe refund adapter accepts a live or a test key

The `stripe.refund` adapter config names `mode: "live"` or `mode: "test"`. The credential must be a Stripe key of that mode, and the payment intent must belong to that mode. A mismatch is refused before anything is sent. The fault drill runs with `mode: "test"`.

**Migration note.** An action created with `mode: "test"` keeps working with its test key. To refund live payments, create an action with `mode: "live"` and store a live key as its secret.

## 2026-10-03

### Notification routes are split into read, write and manage

**Security fix, breaking.** Every method on `/api/v2/notifications` used to need `notifications:read`. A key with that scope could change the alert webhook, the email recipients and send a test webhook.

| Route | Scope | Role |
| --- | --- | --- |
| `GET /`, `GET /unread-count`, `GET /preferences` | `notifications:read` | any member |
| `GET /preferences/me` (new) | `notifications:read`, needs a user | any member |
| `PATCH /preferences/me` (new) | `notifications:write`, needs a user | any member |
| `PATCH /{id}/read`, `PATCH /read-all`, `DELETE /{id}` | `notifications:write` | any member |
| `PATCH /preferences`, `POST /preferences/test-webhook` | `notifications:manage` | admin |

- `notifications:write` is in the default API key scope set.
- `notifications:manage` is privileged. No key receives it by default.
- A member mutes notification types for themselves with `/preferences/me`. A muted type is still raised and still delivered to every channel and every other member.
- An API key has no user and answers `403` `USER_IDENTITY_REQUIRED` on `/preferences/me`.

**Migration note.**

- An OAuth client connected before 2026-10-03 must re-consent before it can mark notifications read, dismiss them or change notification settings.
- An API key with an explicit scope list needs `notifications:write` added to mark read or dismiss, and `notifications:manage` to change workspace notification settings. Until then those calls answer `403`.

### A third adapter, `resend.email_send`

Additive. `POST /api/v2/actions` and `PATCH /api/v2/actions/{ref}` accept `adapter.type: resend.email_send` with the config `{ domain, fromAddresses? }`.

- Impact is counted in `emails`, one per recipient across `to`, `cc` and `bcc`.
- `succeeded` means Resend accepted the send and returned an email id. A read-back that matches grades the effect `confirmed`. Neither grade means the email was delivered.
- A replay is offered for 24 hours, the time Resend keeps an idempotency key.

**Migration note.** None. Existing actions are unchanged.

### Any action can be governed, and every effect returns a grade

An action may carry `impact`, `verify` or `governed: true`. Such an action is governed: each invocation creates an effect, binds its approval to the exact request, reserves its declared quantity against every applicable impact limit before dispatch, and returns a receipt. An action with no declaration and no adapter is unchanged.

Additions:

- `Action` and `ActionVersion` return `governed`, `impact` and `verify`.
- `Effect` returns `grade`: `confirmed`, `acknowledged`, `unknown`, `failed`, `refused`, `denied` or `pending`. `GET /api/v2/actions/effects` accepts `grade` as a filter.
- `ImpactLimit` returns `unit`, and `POST /api/v2/impact-limits` accepts a declared unit as `dimension`.
- Changing or removing what a governed action declares needs the `actions:govern` scope.

**Breaking for typed clients.** `dimension` on `ImpactQuantity`, `ImpactLimit` and `CreateImpactLimitRequest` is no longer a closed enum of `money` and `resource_mutations`. A workspace that declares a unit, such as `emails`, sees that unit name there.

**Migration note.** Treat `dimension` as a string. A workspace that declares no unit sees the same two values as before. Handle an unrecognised `grade` value as you would `unknown`.
