BETA
Skip to content

Notifications

Create, send, and manage push notifications. Notifications can be sent immediately, scheduled for later, or saved as drafts.

Base URL: https://app.krafter.dev/api/v1

List Notifications

Retrieve all notifications for an app, with optional filtering by status.

GET /push/apps/:app_id/notifications?status=...&limit=50&offset=0

Required scope: push:read

Query Parameters

ParameterTypeDefaultDescription
statusstringFilter by status: draft, scheduled, sending, sent, failed (lib/krafter/push/push_notification.ex:44).
limitinteger50Maximum number of notifications to return.
offsetinteger0Number of notifications to skip for pagination.

Example Request

bash
curl "https://app.krafter.dev/api/v1/push/apps/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/notifications?status=sent&limit=20" \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

json
{
  "data": [
    {
      "id": "e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
      "app_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "title": "New Feature Available",
      "body": "Check out our redesigned dashboard with real-time analytics.",
      "icon": "https://example.com/icon.png",
      "image": "https://example.com/banner.png",
      "url": "https://example.com/dashboard",
      "actions": [
        {
          "title": "Open Dashboard",
          "action": "open_url"
        }
      ],
      "data": {
        "feature": "dashboard-v2"
      },
      "targeting": {
        "type": "tags",
        "tags": ["beta"],
        "match": "any"
      },
      "platform_overrides": null,
      "status": "sent",
      "scheduled_at": null,
      "sent_at": "2025-06-14T12:00:00Z",
      "stats": {
        "total_count": 5200,
        "total_batches": 52,
        "completed_batches": 52,
        "sent_count": 4980,
        "failed_count": 220
      },
      "created_at": "2025-06-14T11:55:00Z",
      "updated_at": "2025-06-14T12:00:00Z"
    }
  ]
}

Get Notification

Retrieve details of a specific notification, including delivery stats.

GET /push/apps/:app_id/notifications/:notification_id

Required scope: push:read

Notification stats schema

The stats field is the raw notification.stats jsonb map maintained by the dispatcher (push_dispatch_worker.ex:53-66, push_delivery_worker.ex:154-184). The keys are:

KeyDescription
total_countNumber of subscribers selected by the targeting rule (set when dispatch begins).
total_batchesHow many delivery batches the audience was split into (@batch_size = 50 per push_dispatch_worker.ex:22).
completed_batchesBatches that have finished — equal to total_batches once finalized.
sent_countAtomic counter incremented per successful provider response.
failed_countAtomic counter incremented per provider failure or expired token.

Per-recipient delivered/clicked aggregates live in Analytics (the delivered daily counter is currently always 0 — see that page for why).

Example Request

bash
curl https://app.krafter.dev/api/v1/push/apps/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/notifications/e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

json
{
  "data": {
    "id": "e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
    "app_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "title": "New Feature Available",
    "body": "Check out our redesigned dashboard with real-time analytics.",
    "icon": "https://example.com/icon.png",
    "image": "https://example.com/banner.png",
    "url": "https://example.com/dashboard",
    "actions": [
      {
        "title": "Open Dashboard",
        "action": "open_url"
      }
    ],
    "data": {
      "feature": "dashboard-v2"
    },
    "targeting": {
      "type": "tags",
      "tags": ["beta"],
      "match": "any"
    },
    "platform_overrides": null,
    "status": "sent",
    "scheduled_at": null,
    "sent_at": "2025-06-14T12:00:00Z",
    "stats": {
      "total_count": 5200,
      "total_batches": 52,
      "completed_batches": 52,
      "sent_count": 4980,
      "failed_count": 220
    },
    "created_at": "2025-06-14T11:55:00Z",
    "updated_at": "2025-06-14T12:00:00Z"
  }
}

Create Notification

Create a notification and optionally send it immediately. Set send: true to send right away, provide scheduled_at to schedule for later, or omit both to save as a draft.

POST /push/apps/:app_id/notifications

Required scope: push:write

Status lifecycle

A notification moves through draft → scheduled → sending → finalizing → sent (or failed if every batch fails). Concrete mechanics:

  • draft — default when neither send nor scheduled_at is set. Never dispatched.
  • scheduled — set when scheduled_at is in the future. PushScheduleWorker runs every minute via Oban cron (config.exs:87) and promotes due notifications to sending by calling Push.send_notification/1.
  • sendingPushDispatchWorker is enqueued. It streams matching subscribers in batches of @batch_size = 50 (push_dispatch_worker.ex:22), enqueueing one PushDeliveryWorker per recipient and writing total_count / total_batches / completed_batches: 0 into notification.stats.
  • finalizing — every recipient has a PushDelivery row; the dispatcher atomically claims the row (push_delivery_worker.ex:193-207) so only one worker finalizes.
  • sent / failed — final state. notification.sent_at is stamped and webhook jobs for notification.sent are enqueued (only when status is sent, push_dispatch_worker.ex:97-100).

Daily counters are written separately by PushStatsAggregator, which runs at 02:00 UTC (config.exs:88) and aggregates yesterday's deliveries into push_daily_stats.

Request Body

FieldTypeRequiredDescription
titlestringYesNotification title.
bodystringYesNotification body text.
iconstringNoURL to the notification icon.
imagestringNoURL to a large image displayed in the notification.
urlstringNoURL to open when the notification is clicked.
actionsobject[]NoAction buttons. Each action has title and action fields.
dataobjectNoCustom key-value data payload sent with the notification.
targetingobjectNoWho to send to. Defaults to {"type": "all"}. See Targeting below.
platform_overridesobjectNoPlatform-specific overrides. See Platform Overrides below.
scheduled_atstringNoISO 8601 datetime to schedule delivery.
sendbooleanNoSet to true to send immediately. Defaults to false (saved as draft).

Targeting

The targeting object determines which subscribers receive the notification. The dispatcher selects the audience by switching on a required "type" discriminator (lib/krafter/workers/push_dispatch_worker.ex:131-172). Use one of the five shapes below — any other shape is silently treated as {"type": "all"} with a server-side warning, so a typo in "type" will broadcast to every active subscriber.

ShapeAudience
{"type": "all"}every active subscriber for the app (default when targeting is omitted)
{"type": "user_ids", "ids": ["user-1", "user-2"]}subscribers whose user_id is in the list
{"type": "tags", "tags": ["beta"], "match": "any"}subscribers tagged with at least one of the listed tags
{"type": "tags", "tags": ["beta", "ios"], "match": "all"}subscribers tagged with all of the listed tags
{"type": "platform", "platforms": ["web", "ios"]}subscribers whose registered platform is in the list

Unknown targeting falls through to "all"

A request body such as {"tags": ["beta"]} (without "type"), {"type": "segments", ...}, or any unrecognised shape does not return an error. The dispatcher logs a warning and sends to every active subscriber. Always include the "type" field exactly as shown above.

Segments are not supported

There is no segments targeting shape in the dispatcher today. Any segments field in earlier examples was incorrect — those payloads fell through to "all subscribers".

Platform Overrides

The platform_overrides object allows customizing notification behavior per platform.

FieldTypeDescription
androidobjectAndroid-specific overrides (e.g., channel_id, priority).
iosobjectiOS-specific overrides (e.g., sound, badge, category).

Example Request

bash
curl -X POST https://app.krafter.dev/api/v1/push/apps/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/notifications \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New Feature Available",
    "body": "Check out our redesigned dashboard with real-time analytics.",
    "icon": "https://example.com/icon.png",
    "image": "https://example.com/banner.png",
    "url": "https://example.com/dashboard",
    "actions": [
      {
        "title": "Open Dashboard",
        "action": "open_url"
      },
      {
        "title": "Dismiss",
        "action": "dismiss"
      }
    ],
    "data": {
      "feature": "dashboard-v2"
    },
    "targeting": {
      "type": "tags",
      "tags": ["beta"],
      "match": "any"
    },
    "platform_overrides": {
      "android": {
        "channel_id": "features",
        "priority": "high"
      },
      "ios": {
        "sound": "default",
        "badge": 1
      }
    },
    "send": true
  }'

Example Response

json
// 201 Created
{
  "data": {
    "id": "e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
    "app_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "title": "New Feature Available",
    "body": "Check out our redesigned dashboard with real-time analytics.",
    "icon": "https://example.com/icon.png",
    "image": "https://example.com/banner.png",
    "url": "https://example.com/dashboard",
    "actions": [
      {
        "title": "Open Dashboard",
        "action": "open_url"
      },
      {
        "title": "Dismiss",
        "action": "dismiss"
      }
    ],
    "data": {
      "feature": "dashboard-v2"
    },
    "targeting": {
      "type": "tags",
      "tags": ["beta"],
      "match": "any"
    },
    "platform_overrides": {
      "android": {
        "channel_id": "features",
        "priority": "high"
      },
      "ios": {
        "sound": "default",
        "badge": 1
      }
    },
    "status": "sent",
    "scheduled_at": null,
    "sent_at": "2025-06-14T12:00:00Z",
    "stats": {
      "total_count": 5200,
      "total_batches": 52,
      "completed_batches": 0,
      "sent_count": 0,
      "failed_count": 0
    },
    "created_at": "2025-06-14T12:00:00Z",
    "updated_at": "2025-06-14T12:00:00Z"
  }
}

Track Click

Track when a subscriber clicks a notification.

POST /push/notifications/:notification_id/track

Required scope: push:write (lib/krafter_web/controllers/api/v1/push_controller.ex:42, 397-398).

Auth-gated, despite the name

This endpoint sits inside the api_authenticated pipeline and requires a push:write API key like every other write action. It is not a public endpoint and cannot be called directly from service workers or client-side code without leaking a credential that can also create and send notifications.

In practice this means real browser-side click tracking is not possible today — the only callers that can hit this endpoint are authenticated server-to-server clients. If you need click counts from end-user browsers, route the click through your own backend and forward to this endpoint server-side.

Per-app authorization is enforced

The action list in the ResourcePermission plug (push_controller.ex:51-67) keys off app_id in the URL, which track_click does not have. The controller therefore resolves the notification's app inline and applies the same push_app allow-list check (push_controller.ex:374-432): a key restricted to push app A cannot mark deliveries on push app B even when both live in the same team. Unrestricted keys (no per-app permissions) keep working as before.

Example Request

bash
curl -X POST https://app.krafter.dev/api/v1/push/notifications/e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b/track

Example Response

json
{
  "ok": true
}

Built by Krafter Studio