API reference · v1
Sending API
Send, schedule and track notifications from your backend. One API for your app’s Web, Android and iOS audiences.
https://api.notirabbit.comOn this page
Quickstart
Copy your app secret from App Settings → Keys & IDs. Store it as NOTIRABBIT_APP_SECRET on your backend, then send the request below with your website’s URL.
curl --request POST 'https://api.notirabbit.com/v1/messages' \
--header "X-App-Secret: $NOTIRABBIT_APP_SECRET" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"title": "A new story is ready",
"body": "Read the latest from our newsroom.",
"url": "https://news.example.com/stories/123",
"idempotency_key": "story-123:notification-1"
}'Use a separate test app containing only your own devices for your first send. Each request targets the app’s active subscribers.
{
"ok": true,
"message_id": "019928b0-cc00-7000-8000-000000000001",
"status": "sending",
"duplicate": false
}201 means the message was accepted. It does not mean a device received it. Save the message_id to check its progress . A valid new request inside quiet hours returns 200 with status: "skipped" and no message ID.
Authentication
X-App-Secret: YOUR_APP_SECRETThe secret selects your app and its audience. You do not send an app_id. Workspace owners and admins can view the secret in the dashboard.
Make requests from your server. Keep the secret out of website JavaScript, mobile apps, source control and URLs. The public SDK key does not authorize a send. These endpoints do not grant browser CORS access.
Use the API hostname above, with no extra /api prefix. The same paths on the dashboard hostname return 404; requests and credentials are not redirected.
Send a message
/v1/messagesSend JSON with Content-Type: application/json. For Web, only title, body and url are required. Mobile sends also need an explicit platforms selection. Add media, an expiry window, scheduling or retry protection when you need them.
All backend and CMS sends use this endpoint. New requests follow the app’s quiet hours and configured destination-domain restriction. The old CMS category, minimum-interval and automation controls are retired.
POST /v1/server/events has been removed and returns 404, with no redirect or compatibility alias. Use body instead of summary and idempotency_key instead of event_id. Old event IDs do not reserve keys in this API.
Parameters
| Parameter | Value and behavior |
|---|---|
titleRequired · string | Nonblank title, up to 255 characters. |
bodyRequired · string | Nonblank message text, up to 2,000 characters. |
urlRequired · string | Absolute HTTP(S) click destination, up to 1,024 characters. Must match the app’s domain when its restriction is enabled. |
mobile_urlOptional · string or null | Android/iOS destination, up to 1,024 characters. HTTP(S) or an explicitly allowed custom app scheme. Omitted, blank or null uses url. Web always uses url. |
icon_urlOptional · string or null | Absolute HTTP(S) icon URL, up to 1,024 characters. Omitted or null uses the app’s default icon, if set. |
image_urlOptional · string or null | Absolute HTTP(S) image URL, up to 1,024 characters. Display depends on the platform and device. |
ttlOptional · integer or null | Seconds a push provider may retain the message after sending: 60–2,419,200. Default: 86,400 (24 hours). This does not schedule the send. |
scheduled_atOptional · string or null | Future ISO 8601 timestamp with seconds and an explicit timezone, such as 2026-12-01T12:30:00+03:00. Omitted or null sends immediately. |
expires_atOptional · string or null | Absolute expiry with the same timestamp syntax as scheduled_at. For a new request, it must be after the intended send time and at most 28 days later. Omitted or null uses the app’s Manual & API lifetime, initially 24 hours. |
platformsOptional · string array | Enabled platforms to target: web, android, ios. Omitted selects Web only. Apps without Web must explicitly select an enabled mobile platform. Null, an empty array, an unknown value or an unavailable platform returns 422. |
idempotency_keyOptional · string | Nonblank key, up to 128 characters. Recommended for every integration that retries. Use the same key and content for the same intended send. |
Surrounding whitespace is trimmed and empty nullable fields become null, except that mobile_url preserves its exact bytes and rejects whitespace in a nonempty value. A supplied blank or null idempotency_key is rejected. Unknown fields return 422, including event, event_id, category, published_at, summary, app_id and unsupported targeting fields.
The complete encoded push payload must also fit 2,600 bytes, including tracking, delivery identifiers and the default icon. Valid individual field lengths can still exceed that budget. For iOS targets, the complete APNs body must also fit 4,096 bytes, including the repeated title and body used by the native alert. An oversized request returns 422 with errors.body.
Platform targeting
One app can contain Web, Android and iOS subscriptions. Activate each platform in Platforms before targeting it. Mobile platforms require their platform credentials and an app build with the mobile SDK integrated.
Existing API integrations that omit platforms continue to target Web only, even after mobile platforms are activated. Use ["web"] for an explicit Web selection or ["web", "android"] to include Android. In the dashboard, choose the enabled platforms for each message.
{
"title": "Your technology briefing",
"body": "The latest stories are ready.",
"url": "https://news.example.com/briefing",
"platforms": ["android", "ios"],
"idempotency_key": "briefing-mobile-2026-12-01"
}The selected platforms are saved when a new message is accepted. Activating another platform later does not expand an already accepted or scheduled message. The active devices within those platforms are selected when delivery starts. Historical messages without a saved platform target remain Web-only.
Keep the same platform selection on retries. An omitted selection preserves the original idempotency behavior; adding or changing an explicit selection under the same key can return 409. Statistics initially aggregate the selected platforms.
Mobile destinations
Keep url as the website destination. Add mobile_url when native devices should open a different address. The same message can open the article on the website for Web subscribers and inside the app for iOS or Android subscribers.
First register the app’s custom scheme in App Settings → General → Mobile deep-link schemes, for example newsapp without a colon or slashes. The mobile app must register and handle that scheme too. Up to ten distinct lowercase schemes are allowed, each at most 32 characters. Unsafe or system schemes are rejected.
{
"title": "A new story is ready",
"body": "Read the latest from our newsroom.",
"url": "https://news.example.com/stories/123",
"mobile_url": "newsapp:///stories/123",
"platforms": ["web", "ios"],
"idempotency_key": "story-123:mobile-link-1"
}Custom destinations keep their path, query and fragment unchanged; no UTM parameters are added. HTTP(S) mobile destinations follow the app’s domain restriction and tracking settings. An invalid or unregistered destination returns 422 with errors.mobile_url, even when the current targets are Web-only.
Blank, omitted and null mobile destinations use the regular URL and retain historical retry behavior. A nonempty mobile URL becomes part of the idempotency fingerprint; changing it under the same key returns 409. Pending messages recheck the current scheme policy before enqueue. Already queued payloads are unchanged.
Scheduling
Add scheduled_at to send later. Use Z for UTC or an explicit offset such as +03:00. Include whole seconds; timezone-less dates and fractional seconds are rejected.
{
"title": "Your afternoon briefing",
"body": "The stories to catch up on today.",
"url": "https://news.example.com/briefing",
"scheduled_at": "2026-12-01T12:30:00+03:00",
"idempotency_key": "briefing-2026-12-01"
}Choose a future date when using this example. 12:30:00+03:00 and 09:30:00Z identify the same instant. The response starts with status: "scheduled".
Scheduling is the earliest intended dispatch time. The scheduler checks every minute; queue and provider delays can add time. Exact-second delivery is not guaranteed. You can cancel a scheduled message in the dashboard; this API currently has no update or cancel endpoint.
Quiet hours
Set the daily window in App Settings → API Quiet Hours. Times use Europe/Istanbul. A 23:00–07:00 window crosses midnight: 23:00 is blocked and 07:00 is allowed. Matching start and end times or two empty fields disable the window.
Immediate requests are checked at acceptance; scheduled requests use the intended scheduled_at. A valid request inside the window is skipped: no message or delivery job is created, and nothing is held until morning. Invalid content, destination, schedule or expiry still returns a validation error.
{
"ok": true,
"message_id": null,
"status": "skipped",
"skipped": "quiet_hours",
"duplicate": false
}When a key is supplied, this skip is saved. Retrying the same content and key after quiet hours returns the same skip with duplicate: true. Use a new key only for an intentionally new notification.
This rule applies to new API requests, including CMS sends. Manual dashboard messages are unaffected. Accepted or queued messages are not checked again, so processing delays and provider delivery can cross into quiet hours.
Message expiry
expires_at is the message’s absolute deadline. Without it, the app’s Manual & API lifetime is added to immediate acceptance time or the intended scheduled time. That lifetime starts at 24 hours and can be changed in App Settings. The deadline is saved once; retries and queue delays do not extend it.
ttl limits how long the push provider may retain a notification after sending. Before a new provider request, the worker uses the shorter of that TTL and the remaining message lifetime. A queued job with no usable lifetime is counted as timed_out; its subscriber is not expired.
Expiry cannot recall a notification already sent or guarantee an exact device-display cutoff. Historical messages may have expires_at: null and retain their original TTL-only behavior.
Safe retries
Generate an idempotency_key once for each intended notification and keep it with the request. A UUID or a value such as story-123:notification-1 works. Reuse it after a timeout or temporary error.
200, duplicate: true. The original outcome is returned: the message with its current status, or its saved quiet-hours skip. No new send is created.
409 Conflict. Check your saved request. Use a new key only when you intend a new notification.
{
"ok": true,
"message_id": "019928b0-cc00-7000-8000-000000000001",
"status": "completed",
"duplicate": true
}Without a key, each request is evaluated independently and creates a new message when allowed, even when the title and URL match. Keys are scoped to your app and have no automatic expiry. Normalized content includes trimmed strings, null optional media, the effective TTL, the scheduled UTC instant and an explicitly supplied expiry. Omitted or null expiry does not change older request fingerprints.
Request-shape validation runs before replay lookup, so a malformed retry can return 422. Valid same-content retries preserve their outcome after the schedule or expiry passes, or quiet hours and app settings change. They never recalculate the deadline. Canceled and failed messages keep their key. If the linked message was deleted, a replay returns message_id: null, status: "deleted" and duplicate: true. It never recreates the message.
Destination domains
In App Settings → General, an owner or admin can enable Restrict notification links to this domain. It is off by default. When enabled, the click URL must use the registered domain or its www equivalent.
| Destination | Result |
|---|---|
https://news.example.com/story | Allowed |
https://www.news.example.com/story | Allowed |
https://sports.news.example.com/story | Rejected |
https://news.example.com.evil.com/story | Rejected |
https://news.example.com@evil.com/story | Rejected |
We compare the actual hostname, not the beginning of the URL text. HTTP(S) with its default port is supported; credentials, disguised hosts and nonstandard ports are rejected. External CDN URLs for icons and images are allowed.
A new disallowed URL returns 422 with errors.url. Pending messages are checked again before enqueue if the setting changes. Already queued or delivered notifications cannot be recalled, and redirects later served by the allowed website are outside this check.
Message status
/v1/messages/{message_id}Use the same app secret and the ID from the send response. You can query API, manual and historical CMS messages belonging to that app. Quiet-hours skips have no message to query; replay the same request and key to retrieve a saved skip. Missing, deleted and other apps’ messages return 404.
curl 'https://api.notirabbit.com/v1/messages/019928b0-cc00-7000-8000-000000000001' \
--header "X-App-Secret: $NOTIRABBIT_APP_SECRET" \
--header 'Accept: application/json'{
"ok": true,
"message_id": "019928b0-cc00-7000-8000-000000000001",
"status": "completed",
"source": "api",
"created_at": "2026-09-09T09:00:00.000000Z",
"scheduled_at": "2026-09-09T09:00:00.000000Z",
"started_at": "2026-09-09T09:00:00.000000Z",
"completed_at": "2026-09-09T09:01:00.000000Z",
"expires_at": "2026-09-10T09:00:00.000000Z",
"statistics": {
"sent": 98,
"received": 90,
"clicked": 12,
"failed": 1,
"expired": 1,
"suppressed": 0,
"timed_out": 0,
"delivery_shortfall": 0
}
}Status is draft, scheduled, sending, completed, failed or canceled. skipped and deleted appear only in POST outcomes, not as stored message statuses. The source is api, manual or the historical cms_event. Dates are UTC ISO 8601, or null before that stage. Immediate messages may retain acceptance time in scheduled_at for queue recovery.
| Counter | Meaning |
|---|---|
sent | The push provider accepted the message. This is not a device receipt. |
received | The browser or native SDK reported a push event. This does not prove that display succeeded. |
clicked | A notification click was reported. This does not prove that the destination loaded. |
failed / expired | Terminal delivery failures or expired subscriptions. |
suppressed | Queued jobs skipped because the message was stopped or the subscriber was removed or opted out. Quiet-hours skips do not create jobs and are not counted here. |
timed_out | Queued jobs that reached their message deadline before a provider request. This does not expire the subscription. |
delivery_shortfall | Known audience without final delivery outcomes when recovery closes the message. |
Counters update about every ten seconds while the pipeline is healthy. The browser and mobile SDKs retry retained receipt/click reports when their lifecycle allows, but offline expiry, storage limits and device settings can still leave gaps. completed means processing finished; it does not guarantee every device received a notification. An empty audience can complete with zero sends.
The dashboard can stop a message that is already sending. It stays canceled, while queued jobs and in-flight delivery or engagement can still update its counters. Stopping cannot recall notifications already accepted by a provider or displayed.
Errors & limits
| HTTP status | What to do |
|---|---|
| 401 Unauthorized | Check the app secret. Public keys and secrets from deleted apps do not work. |
| 404 Not Found | Check the API hostname, path and message ID. |
| 409 Conflict | The idempotency key was used with different content. Check the original request. |
| 422 Unprocessable Content | Correct the fields in errors. No new message is created. |
| 429 Too Many Requests | Wait for Retry-After, then retry the same payload and key. |
| 5xx or a network timeout | Acceptance may be unknown. Retry with the same key, increasing delays and jitter; alert after repeated failures. |
{
"message": "The title field is required.",
"errors": {
"title": ["The title field is required."]
}
}Sending is limited to 240 requests per minute per client IP. Status reads have a separate 600 requests per minute per client IP limit. Apps using the same server IP share the allowance. Authentication failures and retries also count.
Delivery behavior
Messages are processed asynchronously. If the immediate queue start fails after acceptance, the saved message stays recoverable and may initially report scheduled. Check status instead of creating another send with a new key.
The audience is read during processing, not frozen when the API accepts the request. Delivery targets active subscribers on the selected platforms. Subscriber IDs, tags, segments and provider dry-run are not supported by this endpoint.
Delivery is at least once. A retry after an uncertain provider acknowledgment can repeat a push even when API acceptance is deduplicated. The message ID helps collapse visible repeats; it is not an end-to-end exactly-once guarantee.
Tracking parameters are added to HTTP(S) click URLs while preserving explicit URL parameters; custom mobile links are unchanged. Keep content concise: the delivery pipeline can shorten content or remove optional media if later app settings make the payload too large.
Back to quickstart