Troubleshooting
Reading Delivery errors, auto-unsubscribe, and common failure causes.
Where to look first
A failed send never throws an error back from
POST /v1/apps/{app_id}/notifications itself - that call only confirms
ZeepNotti accepted the request. Delivery outcome is per-Device, checked via
GET /v1/apps/{app_id}/notifications/{id}/deliveries. Each Delivery has a
status (sent / failed / pending) and, on failure, a last_error
string.
last_error is the raw provider error text, not a stable error code -
its exact wording differs between APNs and FCM and can change if the
provider changes its own message text. Match on substrings /
provider-specific fields below, don't expect a fixed enum.
Auto-unsubscribe: what device.invalid means
When a provider reports a push token as permanently invalid (see the two tables below), ZeepNotti does two things automatically, before you take any action:
- Sets that Device's
subscribedtofalse. - Dispatches a
device.invalidwebhook event instead ofnotification.failed.
This is not a bug or a flaky failure - it means the provider told ZeepNotti
the token will never work again (app uninstalled, token revoked, wrong
gateway - see below). Re-subscribing without a fresh, valid token from the
device will just fail again the same way. Your webhook handler is the
reliable signal here - don't infer "permanently invalid" from last_error
text alone, key off the device.invalid event type.
APNs: permanent vs retryable
APNs reason (appears inside last_error) | Outcome | What it usually means |
|---|---|---|
BadDeviceToken | Permanent - auto-unsubscribed | Token malformed, revoked, or (very commonly) sent to the wrong gateway - see Testing: sandbox vs production |
Unregistered | Permanent - auto-unsubscribed | App was uninstalled from the device |
ExpiredToken | Permanent - auto-unsubscribed | Token no longer valid |
DeviceTokenNotForTopic | Permanent - auto-unsubscribed | Token belongs to a different bundle ID than this App's APNs credentials |
TooManyRequests, InternalServerError, ServiceUnavailable, Shutdown, ExpiredProviderToken | Retryable | Transient APNs-side issue or an expired provider auth token (re-upload APNs credentials if this persists) |
| Any other reason string | Treated as permanent | ZeepNotti does not retry unrecognized APNs reasons |
If you're seeing BadDeviceToken on every send from a specific App, check
that App's apns_environment (sandbox vs production) against how the
client build that produced the token was signed - this is the single most
common cause, not a broken integration.
FCM: permanent vs retryable
FCM error code (appears inside last_error) | Outcome | What it usually means |
|---|---|---|
UNREGISTERED | Permanent - auto-unsubscribed | App was uninstalled, or the token was rotated on the client without re-registering |
INVALID_ARGUMENT | Permanent - auto-unsubscribed | Malformed token, or a token from a different Firebase project than the App's configured credentials |
UNAVAILABLE, QUOTA_EXCEEDED | Retryable | Transient FCM-side issue or you've hit FCM's own sending quota |
HTTP 429, 500, 503 with no matching error code above | Retryable | Same as above, classified by HTTP status when FCM omits a specific code |
| Any other code | Treated as permanent | ZeepNotti does not retry unrecognized FCM codes |
provider_not_configured
A distinct last_error value, unrelated to the token itself: the App has
no APNs credentials configured for an iOS Device, or no FCM credentials
for an Android Device. Fix by completing credential setup for that
platform - the Device's token isn't the problem.
Retryable failures
A failed Delivery with a retryable error (see the tables above) does not
mean you need to resend manually - ZeepNotti' worker retries these with
backoff on its own. Resending the same logical notification yourself risks
a duplicate; use an
Idempotency-Key if
you do need to safely retry the API call itself.