API Behavior Changes in v5.9.5
This page lists every deliberate change in v5.9.5 that an existing integration can observe, so you can check your code against it before upgrading.
This page is maintained through the release cycle
Entries are added as fixes merge and the list is finalized at release. If you are reading it mid-cycle, treat it as current-but-growing rather than frozen.
Looking for the previous release? See API Behavior Changes in v5.9.4, which turned an invalid subscribers.get search field from a masked empty success into an explicit error.
Tier 2: Same call, different results
No request change is needed, but the response values differ. The response shape is unchanged in both cases below.
emailgateway.listrecipientdomainsnow returns data, and its counts mean something different. This endpoint previously returned an emptyRecipientDomainsarray for every account that sends through the gateway API rather than the SMTP relay. It read the ClickHouse event stream, grouping on a recipient-domain field and counting an MTA-acceptance event that only the SMTP relay ingress ever produces, so for API senders there was nothing to group. It now reads the email gateway send queue, which records the recipient domain on every message on both send paths.Three things change for anyone already consuming it.
Sentnow means accepted by Octeth's delivery server, not acknowledged by the receiving MTA.Failednow counts messages whose send attempt failed, which covers both failures on our side and messages the receiving mail server rejected outright during the SMTP conversation; a bounce reported back after the message was already accepted is no longer included.AvgDeliverySec/MinDeliverySec/MaxDeliverySecnow measure queue latency (queued to processed) instead of SMTP session duration, which makes them directly comparable withemailgateway.recipientdomainstatsfor the first time. TheDomainkey is also returned lowercased, soGmail.comandgmail.comare one row instead of two. Because the send queue has no retention window, the endpoint reports historical data immediately with no backfill and no waiting period (issue #2728).emailgateway.recipientdomainstatsis now scoped to the sender domain you pass.DomainIDwas required but was used only to verify ownership and then discarded, so the statistics aggregated across all of the account's sender domains for the given recipient domain. It is now applied as a filter. Accounts with a single sender domain see identical numbers. Accounts with more than one sender domain will see lower numbers than before, because the response no longer includes traffic from their other sender domains. This is what makes the endpoint reconcile with theemailgateway.listrecipientdomainsleaderboard it drills into, which was always sender-domain scoped (issue #2728).
Tier 3: Security closures
These only affect callers doing something that was never intended to work. Listed for completeness and for anyone auditing.
- Segment rules, legacy criteria builder. A
SegmentRuleField(orSegmentRuleOperator) that is not a known subscriber column, aCustomField<n>id, an activity field (Opens/Clicks), aDATE_FORMAT(CustomField<n>, '<format>')date-grouping wrapper, or a recognised operator phrase is now rejected.segment.createreturnsSuccess: falsewithErrorCode: 5andsegment.updatereturnsErrorCode: [7], and no rule is written. Rules already stored with such a field are also dropped when the segment is evaluated. This closes an injection where a crafted rule field reached the legacy criteria builder as raw SQL. The value half of a rule was always escaped; the field and operator halves are now allowlisted at save time and again at evaluation. Segments built in the UI use the JSON rules format and always carry valid fields and operators, so they are unaffected. Only hand-craftedSegmentRuleField[]/SegmentRuleOperator[]values outside the allowlist change behavior (issue #2720).
Upgrade checklist
Do you read
Sent,Failedor the delivery-second fields fromemailgateway.listrecipientdomains? Their meaning changed. If you presentSentas "delivered" or "accepted by the recipient's mail server", relabel it: it now means Octeth accepted the message for delivery. If you presentFailedas a bounce rate, it is now a send-failure count: it includes outright SMTP rejections but excludes bounces reported after acceptance. If you had special-case handling for this endpoint returning an empty array, remove it: it now returns data.Do you zero-fill
DeliverySpeedHistogramorSummaryStatsagainst a fixed list of statuses? Stop. Both are keyed dynamically by the queue statuses actually present in the window, with no zero-fill, and buckets with a count of zero are omitted within a status. Iterate whatever keys are present instead.Bouncedis never a key in either structure.Do you call
emailgateway.recipientdomainstatsfor an account with more than one sender domain? Its numbers are now scoped to theDomainIDyou pass instead of spanning every sender domain on the account. If you were relying on the account-wide total, call it once per sender domain and sum the results.Do you call
segment.createorsegment.updatewithSegmentRuleField[]/SegmentRuleOperator[]values you build or forward from another system? Make sure each field is a known subscriber column, aCustomField<n>id, an activity field (Opens/Clicks), or aDATE_FORMAT(CustomField<n>, '<format>')date-grouping wrapper, and each operator is a recognised phrase (e.g.Contains,Equals to,Is,Is not,Is set,Between). Anything else now returnsSuccess: false(ErrorCode: 5on create,[7]on update) instead of being stored. Callers using the JSONRulesJsonformat are unaffected.
One general note on error codes
ErrorCode and ErrorText are arrays, not scalars, on most endpoints. A rejection typically returns "ErrorCode": [13], not "ErrorCode": 13. Code written as if (response.ErrorCode === 13) will not match, so use response.ErrorCode.includes(13) or your language's equivalent.
subscribers.get is the exception: it returns a scalar ErrorCode (for example "ErrorCode": 4), consistent with its existing codes 1, 2 and 3. Match it as a scalar.

