Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com

- API version: 5.6.0
- Package version: 5.6.0
- API version: 5.7.0
- Package version: 5.7.0

## Requirements

Expand Down
64 changes: 63 additions & 1 deletion docs/DefaultApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,16 @@ Every operation requires either a **REST API Key** (App-scoped, used by ~77% of

`POST /notifications` accepts a top-level `idempotency_key` (UUIDv4) that the server uses for request dedup within a **30-day window**. Pass a freshly-generated UUID per logical send so that network-level retries are safe. Never reuse a key across distinct sends — the server returns the original response instead of acting on the new payload. The hero `create_notification` example below demonstrates the call.

Prefer the bundled `create_notification_with_retry` helper over wiring this up by hand: it generates the key when absent (a caller-provided key is respected), retries 429 / 503 / connection errors with the **same** key (honoring `Retry-After`, exponential backoff otherwise; `max_retries` / `base_delay` configurable), fails fast on other errors, and reports via `was_replayed` whether the server answered from a previously completed request (`Idempotent-Replayed` response header). It is available as a `DefaultApi` method so the call mirrors `create_notification`:

```python
result = api_instance.create_notification_with_retry(notification)
print(result.response.id, result.was_replayed)
```

### Error handling

When a request fails, the SDK raises `onesignal.ApiException`. The HTTP status code is `e.status` (int); the parsed error body is `e.body`. Most envelopes match `{"errors": ["..."]}` (an array of strings) but a few endpoints return `{"errors": [{"code": ..., "title": ..., "meta": {...}}]}` (an array of structured error objects — used by `POST /apps/{app_id}/users` 409 conflict, see `CreateUserConflictResponse`), `{"errors": "..."}` (string), or `{"success": False}` (no `errors` field at all). Robust error-handling code should tolerate all four shapes.
When a request fails, the SDK raises `onesignal.ApiException`. The HTTP status code is `e.status` (int); the parsed error body is `e.body`. Most envelopes match `{"errors": ["..."]}` (an array of strings) but a few endpoints return `{"errors": [{"code": ..., "title": ..., "meta": {...}}]}` (an array of structured error objects — used by `POST /apps/{app_id}/users` 409 conflict, see `CreateUserConflictResponse`), `{"errors": "..."}` (string), or `{"success": False}` (no `errors` field at all). Robust error-handling code should tolerate all four shapes. The `e.error_messages` property does this for you, normalizing every shape to a flat `list[str]` (empty when the body carries no `errors`). To branch on a specific error without hard-coding message strings, test membership against the generated [`OneSignalErrors`](https://github.com/OneSignal/onesignal-python-api/blob/main/onesignal/errors.py) catalog — e.g. `onesignal.OneSignalErrors.NO_TARGETING_SPECIFIED in e.error_messages`.

### Polymorphic 200 from POST /notifications

Expand Down Expand Up @@ -743,9 +750,52 @@ with onesignal.ApiClient(configuration) as api_client:
except onesignal.ApiException as e:
print('Exception when calling DefaultApi->create_notification: %s\n' % e)
print('Status Code: %s' % e.status)
# `e.error_messages` flattens any error-envelope shape to a list[str];
# the raw body remains on `e.body`.
print('Error Messages: %s' % e.error_messages)
print('Response Body: %s' % e.body)
```

#### Using `create_notification_with_retry` (preferred for safe, idempotent retries)

The `create_notification_with_retry` method mirrors `create_notification` but generates the `idempotency_key` for you, transparently retries transient failures (HTTP 429 / 503 / connection errors) with the **same** key, and reports via `was_replayed` whether the server answered from a previously-completed request.

```python
import onesignal
from onesignal.api import default_api
from onesignal.model.language_string_map import LanguageStringMap
from onesignal.model.notification import Notification

# See configuration.py for a list of all supported configuration parameters.
# Some of the OneSignal endpoints require ORGANIZATION_API_KEY token for authorization, while others require REST_API_KEY.
# We recommend adding both of them in the configuration page so that you will not need to figure it out yourself.
configuration = onesignal.Configuration(
rest_api_key = "YOUR_REST_API_KEY", # App REST API key required for most endpoints
organization_api_key = "YOUR_ORGANIZATION_KEY" # Organization key is only required for creating new apps and other top-level endpoints
)


with onesignal.ApiClient(configuration) as api_client:
api_instance = default_api.DefaultApi(api_client)
notification = Notification(
app_id='YOUR_APP_ID',
contents=LanguageStringMap(en='Hello from OneSignal!'),
include_aliases={'external_id': ['YOUR_USER_EXTERNAL_ID']},
target_channel='push',
# No idempotency_key set: the helper generates a UUIDv4 and reuses it across retries.
)
try:
# max_retries / base_delay are optional (defaults: 3 retries, 1.0s backoff base).
result = api_instance.create_notification_with_retry(notification, max_retries=5, base_delay=0.5)
if result.was_replayed:
print('Server replayed a prior send (no duplicate):', result.response.get('id'))
else:
print('Notification created:', result.response.get('id'))
except onesignal.ApiException as e:
# `e.error_messages` flattens any error-envelope shape to a list[str].
print('create_notification_with_retry failed: HTTP %s' % e.status, e.error_messages)
```

### Parameters

Name | Type | Description | Notes
Expand Down Expand Up @@ -1292,6 +1342,18 @@ with onesignal.ApiClient(configuration) as api_client:
```


### Reading the 409 conflict metadata

A `409` from this endpoint returns a `CreateUserConflictResponse` envelope. The `e.error_messages` property flattens each error to its `title`/`code` and omits the structured `meta` object (currently `conflicting_aliases`); parse it from the raw body when you need it:

```python
import json

if e.status == 409:
for err in json.loads(e.body).get("errors", []):
print(err.get("title"), (err.get("meta") or {}).get("conflicting_aliases"))
```

### Parameters

Name | Type | Description | Notes
Expand Down
15 changes: 13 additions & 2 deletions onesignal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""


__version__ = "5.6.0"
__version__ = "5.7.0"

# import ApiClient
from onesignal.api_client import ApiClient
Expand All @@ -26,3 +26,14 @@
from onesignal.exceptions import ApiValueError
from onesignal.exceptions import ApiKeyError
from onesignal.exceptions import ApiException

# import helpers
from onesignal.helpers import CreateNotificationWithRetryResult
from onesignal.helpers import create_notification_with_retry

# OneSignal: surface the idempotent-retry helper as a DefaultApi method so the
# call mirrors create_notification. The function's first parameter (api) binds
# as self; helpers.py stays the single source of truth.
from onesignal.api.default_api import DefaultApi as _DefaultApi
_DefaultApi.create_notification_with_retry = create_notification_with_retry
from onesignal.errors import OneSignalErrors
2 changes: 1 addition & 1 deletion onesignal/api/default_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""
Expand Down
6 changes: 3 additions & 3 deletions onesignal/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""
Expand Down Expand Up @@ -77,7 +77,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None,
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'OpenAPI-Generator/5.6.0/python'
self.user_agent = 'OpenAPI-Generator/5.7.0/python'

def __enter__(self):
return self
Expand Down Expand Up @@ -142,7 +142,7 @@ def __call_api(
# header parameters
header_params = header_params or {}
header_params.update(self.default_headers)
header_params['OS-Usage-Data'] = "kind=sdk, sdk-name=onesignal-python, version=5.6.0"
header_params['OS-Usage-Data'] = "kind=sdk, sdk-name=onesignal-python, version=5.7.0"
if self.cookie:
header_params['Cookie'] = self.cookie
if header_params:
Expand Down
6 changes: 3 additions & 3 deletions onesignal/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""
Expand Down Expand Up @@ -399,8 +399,8 @@ def to_debug_report(self):
return "Python SDK Debug Report:\n"\
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: 5.6.0\n"\
"SDK Package Version: 5.6.0".\
"Version of the API: 5.7.0\n"\
"SDK Package Version: 5.7.0".\
format(env=sys.platform, pyversion=sys.version)

def get_host_settings(self):
Expand Down
28 changes: 28 additions & 0 deletions onesignal/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Generated from inputs/api/error-catalog.json. Do not edit by hand."""


class OneSignalErrors:
"""Sentinel error message strings the OneSignal API can return.

Each constant equals the literal message the server emits, so you can
test membership against the normalized list from
``ApiException.error_messages``::

if OneSignalErrors.NO_TARGETING_SPECIFIED in e.error_messages:
...

Note: 200-status sentinels such as NO_SUBSCRIBERS arrive on a
successful response (CreateNotificationSuccessResponse.errors), not via
the exception accessor — read the response's ``errors`` field for those.
"""

# HTTP 403 | retryable: no | emitted by: any API-key-authenticated endpoint (REST or Organization key) | note: Generic auth-failure message the public api.onesignal.com edge returns for any invalid or mismatched key — REST or Organization — so a single sentinel covers both. Supersedes the Rails-monolith INVALID_REST_API_KEY / INVALID_USER_AUTH_KEY strings, which the public host no longer returns verbatim. Note the double space after 'denied.'
INVALID_API_KEY = "Access denied. Please include an 'Authorization: ...' header with a valid API key (https://documentation.onesignal.com/docs/en/keys-and-ids#api-keys)."
# HTTP 400, 404 | retryable: no | emitted by: POST /notifications/{id}/history, POST /notifications/{id}/messages, GET /notifications/{id} (export) | note: Verified live 2026-06-16: GET /notifications/{bogus-uuid} returns 404 with this exact message.
NOTIFICATION_NOT_FOUND = "Notification not found"
# HTTP 200 | retryable: no | emitted by: POST /notifications | note: Returned with HTTP 200 OK (id is empty), not an error status. The flagship case for the errorMessages accessor — lets callers distinguish a sent notification from a no-op without parsing the polymorphic 200 body.
NO_SUBSCRIBERS = "All included players are not subscribed"
# HTTP 400 | retryable: no | emitted by: POST /notifications | note: Verified live 2026-06-16: a no-targeting POST /notifications returns 400 with this exact message.
NO_TARGETING_SPECIFIED = "You must include which players, segments, or tags you wish to send this notification to."
# HTTP 503 | retryable: yes | emitted by: any endpoint (pgbouncer rejection) | note: Transient DB/pgbouncer failure — the canonical retryable sentinel.
SERVICE_UNAVAILABLE = "Service temporarily unavailable"
47 changes: 46 additions & 1 deletion onesignal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""

import json


class OpenApiException(Exception):
"""The base exception class for all OpenAPIExceptions"""
Expand Down Expand Up @@ -123,6 +125,49 @@ def __str__(self):

return error_message

@property
def error_messages(self):
"""The error messages carried by the response body, normalized to a
flat ``list[str]`` regardless of which envelope shape the API returned
(``{"errors": "..."}``, ``{"errors": ["..."]}``,
``{"errors": [{"code": ..., "title": ...}]}``, or an object map such as
``{"errors": {"invalid_aliases": {...}}}``, surfaced as ``"<key>: <value>"``
entries). Returns an empty list when the body is not a recognizable error
envelope. The raw body remains available on ``self.body``.
"""
parsed = self.body
if isinstance(parsed, (str, bytes, bytearray)):
try:
parsed = json.loads(parsed)
except (ValueError, TypeError):
return []
if not isinstance(parsed, dict):
return []

errors = parsed.get("errors")
if isinstance(errors, str):
return [errors]
if isinstance(errors, list):
messages = []
for e in errors:
if isinstance(e, str):
messages.append(e)
elif isinstance(e, dict):
message = e.get("title") or e.get("code")
if message is not None:
messages.append(message)
return messages
if isinstance(errors, dict):
# Object-shaped envelopes (e.g. {"invalid_aliases": {...}}) carry data
# under arbitrary keys; surface each so it isn't silently dropped. Key
# order is unspecified, so sort for deterministic output.
messages = []
for key, value in errors.items():
rendered = value if isinstance(value, str) else json.dumps(value, separators=(",", ":"))
messages.append("{}: {}".format(key, rendered))
return sorted(messages)
return []


class NotFoundException(ApiException):

Expand Down
98 changes: 98 additions & 0 deletions onesignal/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Helpers for common OneSignal API usage patterns."""

import collections
import time
import uuid

import urllib3

from onesignal.exceptions import ApiException


CreateNotificationWithRetryResult = collections.namedtuple(
'CreateNotificationWithRetryResult', ['response', 'was_replayed']
)

_RETRYABLE_STATUSES = (429, 503)
_MIN_BASE_DELAY = 1.0
_MAX_BASE_DELAY = 60.0


def _header_value(headers, name):
if not headers:
return None
name = name.lower()
try:
items = headers.items()
except AttributeError:
return None
for key, value in items:
if str(key).lower() == name:
return value
return None


def _was_replayed(headers):
value = _header_value(headers, 'idempotent-replayed')
return value is not None and str(value).strip().lower() == 'true'


def _retry_delay(headers, attempt, base_delay):
retry_after = _header_value(headers, 'retry-after')
if retry_after is not None:
try:
return max(float(retry_after), 0.0)
except (TypeError, ValueError):
pass
return base_delay * (2 ** attempt)


def create_notification_with_retry(api, notification, max_retries=3, base_delay=1.0):
"""Create a notification with safe, idempotent retries.

Ensures ``notification.idempotency_key`` is set (generating a UUIDv4 when
absent) so the server can deduplicate, then calls ``create_notification``.
Transient failures (HTTP 429, HTTP 503, or connection-level errors) are
retried up to ``max_retries`` times with the SAME idempotency key,
honoring the ``Retry-After`` response header when present and falling back
to exponential backoff (``base_delay * 2**attempt`` seconds) otherwise.
Other errors are raised immediately.

:param api: a ``DefaultApi`` instance
:param notification: the ``Notification`` to send; an existing
``idempotency_key`` is respected, never overwritten
:param max_retries: retries after the initial attempt (default 3)
:param base_delay: backoff base in seconds when ``Retry-After`` is absent;
clamped to [1.0, 60.0]
:return: ``CreateNotificationWithRetryResult`` with ``response`` (the
``CreateNotificationSuccessResponse``) and ``was_replayed`` (True when
the server answered from a previously completed request, as signaled
by the ``Idempotent-Replayed`` response header)
"""
if not getattr(notification, 'idempotency_key', None):
notification.idempotency_key = str(uuid.uuid4())

# Clamp the backoff base so a stray value can neither hammer the API
# (too small) nor stall the caller for an unbounded stretch (too large).
base_delay = min(_MAX_BASE_DELAY, max(_MIN_BASE_DELAY, base_delay))

attempt = 0
while True:
try:
data, _status, headers = api.create_notification(
notification, _return_http_data_only=False
)
return CreateNotificationWithRetryResult(
response=data, was_replayed=_was_replayed(headers)
)
except ApiException as e:
if e.status not in _RETRYABLE_STATUSES or attempt >= max_retries:
raise
delay = _retry_delay(e.headers, attempt, base_delay)
except urllib3.exceptions.HTTPError:
if attempt >= max_retries:
raise
delay = base_delay * (2 ** attempt)
if delay > 0:
time.sleep(delay)
attempt += 1
2 changes: 1 addition & 1 deletion onesignal/model/api_key_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""
Expand Down
2 changes: 1 addition & 1 deletion onesignal/model/api_key_tokens_list_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com # noqa: E501

The version of the OpenAPI document: 5.6.0
The version of the OpenAPI document: 5.7.0
Contact: devrel@onesignal.com
Generated by: https://openapi-generator.tech
"""
Expand Down
Loading