Push notification APIs sit between product workflows, backend services, mobile applications, browsers, and provider-specific delivery systems. That makes versioning more than a matter of changing a URL. A small change to a target field, payload shape, authentication rule, or retry response can affect scheduled campaigns, transactional messages, device registrations, and client behaviour at the same time.
A sound versioning strategy makes change visible, bounded, and reversible. It gives API consumers a stable contract while allowing the platform to improve validation, targeting, observability, and delivery features. This guide explains the main decisions, a practical migration process, and the mistakes that most often cause avoidable disruption.
Decide what needs versioning
Not every change requires a new major API version. Start by distinguishing additive changes from breaking changes.
Usually compatible changes include:
- Adding an optional request field
- Adding a response field that clients are required to ignore
- Adding a new target type without changing existing target semantics
- Improving documentation or error details while preserving the existing contract
- Adding a new event or webhook type when consumers are not required to handle it
Potentially breaking changes include:
- Renaming or removing a required field
- Changing the meaning of an existing field
- Changing a field from a string to an object, or vice versa
- Making previously accepted input invalid
- Changing default targeting, delivery, or retention behaviour
- Altering authentication or idempotency rules
- Returning an incompatible response structure
Push APIs need particular care because a request can be accepted before a notification reaches a device. The API contract should describe what acceptance means, while client documentation should explain how the notification is handled after delivery. Do not silently change an accepted request into a different notification because a new client or provider happens to interpret the payload differently.
Choose a versioning model
There are three common approaches: a version in the path, a version in a media type or header, and a version selected by account or configuration. Each can work, but each creates different operational costs.
Path versioning is easy to discover and operate. A request such as POST /v1/notifications makes the selected contract visible in logs, dashboards, access policies, and support conversations. It also makes parallel migrations straightforward. The trade-off is that every version can become a permanent route unless retirement is actively managed.
Header or media-type versioning keeps the resource path stable and can express more granular compatibility choices. However, it is less visible when engineers inspect requests, and clients can accidentally omit or overwrite the header. It also requires consistent tooling and documentation across SDKs, gateways, and observability systems.
Account-level or configuration-based versioning can simplify client requests, but it makes behaviour less obvious. A single request may behave differently in development and production if environments have different settings. It is better suited to controlled platform migrations than to a public contract that many independent consumers use.
For most developer-facing notification APIs, explicit path versioning is a practical default. Whatever model you choose, document how the version is selected, which parts of the contract it controls, and whether authentication, rate limits, error formats, and idempotency rules are versioned with it.
Define the contract around intent
A notification request should express product intent rather than expose provider-specific internals wherever possible. In HoneyNotify, server-side sends use POST /v1/notifications with a Bearer API key and an Idempotency-Key. A notification requires a title, body, and target. Targets can address a device, user, tag, segment, or all enabled devices.
That contract gives a useful boundary for versioning. A future version might add richer content or delivery options, but it should preserve the meaning of an existing title, body, and target unless the version explicitly changes that meaning. Provider tokens, platform-specific fields, and transport details should be isolated in documented extension areas rather than scattered through the core request.
Before releasing a new version, write down:
- Required and optional fields
- Accepted data types, length limits, and character rules
- Target resolution and what happens when a target matches nothing
- Authentication requirements
- Idempotency behaviour and the period during which keys are recognised
- Synchronous response meaning
- Error categories and whether clients may retry them
- Payload handling expectations on iOS, Android, and Web Push
- Deprecation and support dates
This contract should be tested as a set of examples, not only described in prose. Include valid requests, invalid requests, duplicate submissions, empty target results, and payloads containing characters or values that are important to your product.
Version the server contract and client behaviour separately
A server API version does not automatically solve client compatibility. Client SDKs cover device registration, identity, token lifecycle, payload handling, and lifecycle events across iOS, Android, and Web Push. Those areas change on different schedules from the send API.
For example, a new server payload field may be safely ignored by an older client, while a changed field may require a new SDK release. A new target type may work immediately on the server but still need client-side identity or registration changes before it is useful. Treat these as separate compatibility questions:
- Can the server accept and validate the request?
- Can the delivery system represent the requested notification?
- Can the installed client receive it?
- Can the client render or act on it safely?
- Can an older client ignore unknown fields without failing?
Use capability negotiation where it adds real value. A client registration or capability record can indicate whether a device supports a feature, allowing the server to choose an appropriate payload. Avoid using version numbers as a substitute for capabilities when support can vary by platform, app build, permissions, or device state.
Plan a migration instead of switching suddenly
A major version should have a migration path before it is announced. First, inventory all callers, including scheduled jobs, background workers, internal tools, customer integrations, and test environments. Search for hard-coded request fields, response parsing, retry logic, and assumptions about target identifiers.
Next, publish a compatibility matrix. Show which client or service versions can call each API version, which features are available, and whether a migration requires a server change, a client release, or both. Keep the matrix tied to behaviour rather than vague labels such as “legacy” or “modern”.
A controlled migration commonly follows this sequence:
- Add the new contract without changing the old one
- Add server support for both versions
- Update shared libraries and SDKs first
- Run contract tests against both versions
- Move a small, observable set of traffic
- Compare acceptance, validation, retry, and delivery-related signals
- Migrate remaining callers
- Announce a deprecation date and removal conditions
- Remove the old version only after usage has fallen to an agreed level
For sends, preserve idempotency during retries and migrations. If a worker retries a request after a timeout, it should not create a second notification merely because the request was routed through a different version. Define whether idempotency keys are scoped by endpoint, version, account, or some combination, and test the rule explicitly.
Make observability version-aware
Version fields should appear in request logs, metrics, traces, audit records, and support tooling. At minimum, track request volume, validation failures, authentication failures, retryable failures, latency, and downstream delivery outcomes by API version and client application.
Do not treat request acceptance as proof that a user saw a notification. Keep API-level outcomes distinct from provider and device outcomes. This distinction helps identify whether a migration introduced a schema problem, a targeting problem, a token lifecycle issue, or client handling regression.
Set an alert for unexpected traffic on a deprecated version, but avoid relying only on aggregate volume. A small number of important transactional sends may matter more than a large number of low-priority messages. Include account, environment, platform, and application version in dashboards where privacy and access controls permit.
Common mistakes
Treating every change as a new major version
Excessive major versions increase testing, documentation, and support work. Use additive changes when they genuinely preserve existing semantics. Reserve a major version for a meaningful contract break.
Hiding breaking behaviour behind feature flags
Feature flags are useful for rollout, but they can make the same API version behave differently for different accounts. If a flag changes the public contract, expose its state clearly and define how it will be retired.
Reusing identifiers across incompatible meanings
A device ID, user ID, tag, or segment should not silently change meaning between versions. If an identifier migration is unavoidable, support an explicit translation period and make failures diagnosable.
Imported OneSignal subscription IDs can remain as device IDs when matching provider tokens later register, but token rotation or another provider environment can prevent matching. A migration must therefore account for token lifecycle and environment differences rather than assuming that an imported identifier guarantees future association.
Ignoring old clients
Older iOS, Android, and Web Push clients may receive a new payload even when they cannot act on it. Use backwards-compatible payloads, capability checks, and safe defaults. Test app versions that are still active, not just the newest SDK build.
Deprecating without evidence
A sunset date without usage data is a guess. Measure version traffic, identify owners, contact affected teams, and provide a documented replacement. Keep the old path available for a defined period when the operational risk of immediate removal is high.
Conclusion
Version a push notification API around stable product intent, explicit compatibility rules, and observable migrations. Choose a versioning model that makes requests easy to understand, keep server and client versioning distinct, preserve idempotency, and test device and token lifecycle assumptions. A new API version should give teams a safer contract, not merely a new route.
