> This page is for SDK, version Legacy.
> For other versions, use one of these documentation indexes:
> - Current (default): https://buildwithfern.com/learn/sdks/current/llms.txt
> - Legacy: https://buildwithfern.com/learn/sdks/legacy/llms.txt

> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt.

# Changelog

## October 19, 2026

## 2.30.6

**`(fix):`** Requests whose path contains a `.` or `..` segment (including `%2e` forms) now throw an
`InvalidArgumentException` before being sent. A path parameter value of `.` or `..` could
otherwise change which endpoint the request was sent to, with or without
`encode-path-params`.

## October 16, 2026

## 2.30.5

**`(fix):`** `WebhooksHelper.verifyRequest` now parses `name=value` settings headers when
`webhook-signature.algorithm-header` sets `key-id-parameter`/`algorithm-parameter`
(e.g. Twilio's `X-Twilio-Webhooks-Signature-Settings: key=<id>,algorithm=HMAC_SHA256`),
so the key resolver receives the bare key ID instead of `key=<id>`. Requires IR 67.33.0.

## October 13, 2026

## 2.30.4

**`(fix):`** The generated `WebhooksHelper` now consumes `algorithm-header` from `api.settings.webhook-signature`:
a request-level `verifyRequest` helper reads the configured signature header first (missing or empty
fails early with "could not run: missing signature header"), then the algorithm-selection header
(e.g. `X-Twilio-Webhook-Signature-Settings: <key_id>;HMAC_SHA256`) to pick the HMAC algorithm from
the configured wire values and hand the key ID to an application-supplied secret resolver. When the
header is absent the configured default algorithm and the plain secret are used, so legacy
auth-token flows keep working. The lower-level `verifySignature` is unchanged and still returns
`false` without throwing.

## 2.30.3

**`(fix):`** Fix the generated `OAuthTokenProvider` so that it compiles under phpstan (level max) when the
token endpoint's `access_token` / `expires_in` response properties are optional or nullable
(e.g. OpenAPI specs that do not list them as `required`). The provider now throws the SDK's
base exception when the response carries no access token, and only computes `expiresAt` when
`expires_in` is present.

## October 9, 2026

## 2.30.2

**`(fix):`** `reference.md` is now generated for every output mode, matching the Go and Python generators. It was
previously skipped unless the CLI passed a snippet path (`local-file-system` output without
`snippets: path:` in generators.yml). `README.md` is unchanged and still requires snippets.

## October 8, 2026

## 2.30.1

**`(fix):`** Parsing an XML (TwiML) element without a text property keeps the text before its first child in the
ordered content, so `Gather::fromXml('<Gather>Press a key, then <Say>one</Say></Gather>')` round-trips
instead of dropping `Press a key, then `. Types with a text property (e.g. `Say`) are unchanged: that
text is still the property's value.

## October 7, 2026

## 2.30.0

**`(feat):`** XML (TwiML) types can now carry XML comments. `comment($text)` adds `<!--text-->` inside the element at
its position in the ordered content (interleaved with `addText` segments and child elements), while
`commentBefore($text)` / `commentAfter($text)` render a comment immediately before or after the element
itself (as a sibling in its parent, or around the root element after the XML declaration). Comments in
parsed documents are preserved by `fromXml` and round-trip through `toXml`; `XmlElement::addComment` and
the new `XmlComment` class expose the same for untyped elements.

## 2.29.0

**`(feat):`** Generate a unit test file per XML-encoded (TwiML) type alongside the models, covering
fromXml/toXml round trips, escaping, unknown attributes/children, open enum passthrough,
child ordering, and rejection of malformed XML, wrong root elements and DOCTYPE declarations.

## 2.28.3

**`(fix):`** XML (TwiML) constructors and fluent child methods accept enum cases for enum-typed attributes and
text, e.g. `new Break_(['strength' => BreakStrength::Weak])` or `['record' => [DialRecordItem::RecordFromAnswer]]`,
in addition to the raw string values. Previously passing the enum instance threw
`TypeError: Cannot assign BreakStrength to property Break_::$strength of type ?string`.

**`(fix):`** List-valued XML (TwiML) attributes accept a single space-separated string in addition to
an array, matching the legacy call style: `new Gather(['input' => 'speech dtmf'])` and
`$response->gather(['input' => 'speech dtmf'])` no longer throw a `TypeError`.

## October 6, 2026

## 2.28.2

**`(fix):`** List-valued XML (TwiML) attributes accept a single space-separated string in addition to
an array, matching the legacy call style: `new Gather(['input' => 'speech dtmf'])` and
`$response->gather(['input' => 'speech dtmf'])` no longer throw a `TypeError`.

## 2.28.1

**`(fix):`** Enum-typed attributes and text on XML (TwiML) types are open on the wire: `fromXml` keeps a value the enum does
not declare (e.g. a voice added after the SDK was generated) instead of throwing, and it round-trips through
`toXml`. This matches how the same enums already behave in JSON.

## 2.28.0

**`(feat):`** Add opt-in `userAgentOnly` config. When enabled, generated clients send the `User-Agent`
header (including the structured form from `includePlatformHeaders`) but omit the
`X-Fern-Language`, `X-Fern-SDK-Name`, and `X-Fern-SDK-Version` headers. Disabled by default,
so existing generated output is unchanged. `omitFernHeaders` still takes precedence and drops
the `User-Agent` too.

## 2.27.0

**`(feat):`** The generated `WebhooksHelper` HMAC verifier now accepts a per-call algorithm override via an optional `algorithm` parameter on `verifySignature`,
one of `sha1`, `sha256`, `sha384` or `sha512`. When omitted, the algorithm configured in
`webhook-signature` is used, so existing callers are unchanged. This lets one endpoint verify both
legacy HMAC-SHA1 (auth-token) and HMAC-SHA256 (shared-key) signed webhooks.
The verifier also logs a warning distinguishing an early failure (missing signature header) from a
failed verification (signature mismatch); it still returns `false` and never throws.

## 2.26.0

**`(feat):`** XML (TwiML) types keep text segments, typed child elements and raw `XmlElement` children in a
single ordered content list, so `toXml()` emits them in the order they were added
(`$response->say(['message' => 'a']); $response->addChild(new XmlElement('Custom')); $response->say(['message' => 'b']);`
yields `<Say>a</Say><Custom/><Say>b</Say>`) and `fromXml()` preserves document order, including
the position of unknown elements. Adds `addText(string)` to XML types and `XmlElement` for mixed
content such as `<Say>Hi <break strength="weak"/> world</Say>`. Typed child properties and
additional children remain available and are reconciled into the content order on serialization.

## 2.25.1

**`(fix):`** XML-encoded (TwiML) types now include the `<?xml version="1.0" encoding="UTF-8"?>`
declaration by default when serialized to XML; pass the declaration flag as `false`
to omit it. Nested child elements and raw `XmlElement`s are still written without it.

## 2.25.0

**`(feat):`** Add support for SDK variables (`x-fern-sdk-variables` / `variables` in `api.yml`). Each variable
becomes an optional, named constructor parameter on the root client (also settable through the
`options` array and per request). Path parameters bound to a variable are removed from endpoint
method signatures and request classes and resolved from the client instead. String variables
that declare an environment variable fall back to `getenv()` when no value is passed, and a
clear `InvalidArgumentException` is thrown when a bound variable is still unset at call time.

## October 2, 2026

## 2.24.2

**`(fix):`** Fix `JsonSerializableType::jsonDeserialize` throwing `Unexpected non-string type for date.`
when a `#[Date]`-annotated property receives an explicit JSON `null`. Nullable date and
date-time fields now deserialize `null` to `null`, matching how omitted keys and the
serializer already behave; non-string, non-null values are still rejected.

## September 29, 2026

## 2.24.1

**`(chore):`** Bump bundled @fern-api/generator-cli to 0.10.3. When the hosted auto-version analysis of the
full SDK diff fails, generator-cli now retries chunk-by-chunk instead of silently applying a
PATCH bump; any remaining unavailable or incomplete analysis adds a "Version bump not verified"
notice to the SDK PR body and disables automerge.

## September 25, 2026

## 2.24.0

**`(feat):`** Inline documentation on XML-encoded (TwiML) types: constructors list every documented
`$values` key with its description, fluent child methods carry the child type's
description and per-key docs for the `$attributes`/child array shape, and text
parameters include the text property's description.

## September 24, 2026

## 2.23.0

**`(feat):`** Generate the shared `WebhooksHelper` from the API-wide `api.settings.webhook-signature`
setting in `generators.yml` (IR `sdkConfig.webhookSignatureVerification`). The helper is
emitted even when the API definition models no webhooks; webhook-specific signature
overrides still produce their own named helpers.

## September 22, 2026

## 2.22.2

**`(chore):`** Bump bundled @fern-api/generator-cli to 0.10.1. Auto-versioned MAJOR/MINOR bumps no longer
produce a version-only `changelog.md` block when the AI analysis returns an empty changelog
entry; the PR description, version bump reason, or commit message body is used instead.

## 2.22.1

**`(fix):`** Honor the org-level whitelabel setting (`readmeConfig.whiteLabel`) and the `apiName`/`disabledSections` (IR `disabledFeatures`) readme
config options when generating README.md. Previously the "Built with Fern" badge was always emitted.

## September 21, 2026

## 2.22.0

**`(feat):`** Object types with XML encoding metadata (e.g. TwiML) now extend `XmlSerializableType` and
generate `toXml()`/`toXmlElement()`, `fromXml()`/`fromXmlElement()`, and fluent child builder
methods (`$response->say('Hello', ['voice' => 'man'])`). Attributes, text, namespaces, prefixes,
wrapped lists, separator-delimited lists and enums round-trip; unknown attributes and children
are preserved and re-emitted, and parsing rejects DTDs, malformed input and wrong root elements.
XML types add `ext-dom`/`ext-libxml` to `composer.json` and a `Core\Xml` runtime.

## September 15, 2026

## 2.21.0

**`(feat):`** Add `rejectEmptyDateTimeStrings` (opt-in). `deserializeDateTime("")` returned
the *current wall-clock time* rather than raising: PHP's `DateTime`
constructor treats an empty string as "construct for the current moment" and
does not throw, so the surrounding catch never fired. Any API that sends `""`
to mean "not set" therefore produced a plausible, entirely fabricated
timestamp with no way for the caller to detect it — a value that can be
written back to a database and is indistinguishable from real data
afterwards. A malformed string was always handled correctly; it was
specifically the empty string that slipped through. The sibling
`deserializeDate` already rejects `""`, so this also removes an
inconsistency between the two. Opt-in because it turns a value callers
currently receive into an exception.

## August 20, 2026

## 2.20.4

**`(fix):`** Honor endpoint-level `retries: { disabled: true }` (`x-fern-retries`). Requests for those
endpoints pin `maxRetries` to 0, so they are never retried regardless of the client-level or
per-request retry options.

## 2.20.3

**`(fix):`** Fixed generated WireMock stubs for OAuth client credentials APIs whose token endpoint is
itself marked as authenticated: the token (and refresh) endpoint stub no longer requires
an `Authorization` header, since the token request is made to obtain the token and cannot
carry one yet.

## August 18, 2026

## 2.20.2

**`(fix):`** Global headers typed as a `literal<"...">` with an `env` fallback no longer throw when
neither the constructor parameter nor the environment variable is set. The literal is
known at compile time, so it is now used as the header's default value.

## August 13, 2026

## 2.20.1

**`(fix):`** Render an empty body in snippets for an example that omits a request body the API does not
require. The generated request wrapper still requires the body, so the snippet left the
required key out and did not typecheck.

## 2.20.0

**`(feat):`** Add a `respectOptionalRequestBody` option. When enabled, an endpoint whose request body the API
does not require lets the caller leave that body out of the call, and such a call sends neither a
body nor a `Content-Type` header. `refund(string $id, RefundRequest $request)` becomes
`refund(string $id, ?RefundRequest $request = null)`, and a body carried in a request wrapper
becomes an omittable property, so `new RefundWithHeaderRequest([])` no longer throws. Snippets
that supply no body render as `$client->bulkRefund();`. The option defaults to false, so existing
signatures, output, and snippets are unchanged.

## August 9, 2026

## 2.19.0

**`(feat):`** Add an opt-in `preferExplicitAuth` configuration option. When enabled (and the API composes
OAuth client-credentials with basic auth via `auth: any`), auth credentials passed
explicitly to the client constructor take precedence over environment-variable
defaults when selecting the auth scheme — e.g. explicitly provided basic auth
credentials win over OAuth client ID/secret environment variables. Disabled by
default, so existing generated output and runtime behavior are unchanged.

## August 8, 2026

## 2.18.2

**`(fix):`** Fixed wire tests for APIs with basic auth schemes that use custom parameter names
(e.g. `accountSid`/`authToken`): the generated test client constructor now uses the
scheme's actual parameter names instead of hardcoded `username`/`password`.
Also fixed WireMock stubs for `auth: any` APIs combining Basic and OAuth/Bearer:
stubs now expect the Bearer `Authorization` header the client actually sends instead
of the Basic header.

## August 5, 2026

## 2.18.1

**`(fix):`** The generated `composer.json` version (and the fallback User-Agent header) now honors
the version passed via `fern generate --version` for `local-file-system` output,
instead of always defaulting to `0.0.0`. The Composer package name also falls back to
the CLI-provided package name when `packageName` is not set in `generators.yml`.

_Showing the 20 most recent of 118 entries. Append `/llms.txt` to the changelog URL for the complete index._