PushizoDocs
Göç

OneSignal'den Pushizo'ya göç

Pushizo, OneSignal'in kavram modelini (app, abonelik, kullanıcı/alias, tag, segment, şablon, journey) ve SDK yüzeyini (initialize → login → User.addTags → Notifications.requestPermission → Session.sendEvent) bilinçli…

Pushizo, OneSignal'in kavram modelini (app, abonelik, kullanıcı/alias, tag, segment, şablon, journey) ve SDK yüzeyini (initialize → login → User.addTags → Notifications.requestPermission → Session.sendEvent) bilinçli olarak korur. Bu rehber mevcut bir OneSignal uygulamasını kesintisiz taşımanın adımlarını, alan/uç eşlemelerini ve SDK değişikliklerini anlatır.

Araçlar: npm run migrate:onesignal (scripts/migrate-onesignal.ts), CSV içe aktarıcı (packages/db/src/data-transfer.ts → mapOneSignalRow), Postman koleksiyonu (api/postman_collection.json).

1. Genel plan

AdımSüreÇıktı
1. Hesap, organizasyon ve app oluşturma10 dkapp_… kimliği
2. Kanal kimlik bilgileri (APNs .p8, FCM servis hesabı, VAPID, e-posta/SMS)30 dkKanallar active
3. Player/abonelik göçü (migrate:onesignal → CSV → içe aktarma)dosya boyutuna göreKullanıcılar, tag'ler, iOS/Android/e-posta/SMS abonelikleri
4. Sunucu entegrasyonu (REST → Pushizo public API / sunucu SDK'ları)1–3 günBildirim, kullanıcı, olay çağrıları
5. İstemci SDK değişimi (Web/iOS/Android/Flutter, React Native/Capacitor/Cordova/.NET MAUI/Unity)1–2 günYeni sürüm yayını
6. Çift yazma dönemi ve kesinti2–4 haftaOneSignal kapatılır

2. Hesap ve app oluşturma

  1. Dashboard'da kayıt olun (/register), e-posta doğrulaması ve isteğe bağlı MFA'yı tamamlayın. Her hesap bir organizasyon altında çalışır; organizasyon OneSignal'deki "hesap", app ise OneSignal app'inin karşılığıdır.
  2. Başlangıç sihirbazı (/<org>/getting-started) ile app'i oluşturun ya da public API'den: POST /v1/apps (org geneli admin anahtarı gerekir).
  3. Organizasyon → API anahtarları'ndan bir sunucu anahtarı (hs_live_…) üretin. Kapsamlar OneSignal REST API anahtarından daha incedir: notifications:write, users:read, users:write, events:write, segments:read, analytics:read, admin. Göç için admin kapsamlı, IP izin listeli ve bitiş tarihli bir anahtar önerilir.
  4. Kanallar sayfasında OneSignal'de kullandığınız kimlik bilgilerini aynen girin:
OneSignal ayarıPushizo kanalıNotlar
Apple → .p8 anahtarı, Key ID, Team ID, Bundle IDiOS Push (ios_push)Ortam sandbox / production; aynı .p8 her iki tarafta çalışır.
Google → Firebase servis hesabı JSONAndroid Push (android_push)Aynı Firebase projesi kullanılır; cihaz token'ları geçerliliğini korur.
Web → Safari/Chrome ayarlarıWeb Push (web_push)Pushizo kendi VAPID çiftini üretir; mevcut tarayıcı abonelikleri taşınamaz (bkz. §4.3).
E-posta (SendGrid vb.)E-posta (email)SMTP / Amazon SES / Resend; gönderici alan adını yeniden doğrulayın (SPF/DKIM/DMARC).
SMS (Twilio)SMS (sms)Twilio hesap bilgileri aynen; Netgsm de desteklenir.

Her kanalda Doğrula düğmesi (POST /v1/apps/{app_id}/channels/{channel}/validate) sağlayıcıya test bağlantısı yapar.

3. Player (abonelik) göçü

3.1 npm run migrate:onesignal

Araç OneSignal "View devices" API'sinden (GET /players, 300'lük sayfalar) ya da büyük uygulamalar için POST /players/csv_export ile cihazları çeker, Pushizo'nun onesignal_players CSV biçimine yazar ve istenirse doğrudan içe aktarmayı başlatıp tamamlanana kadar izler.

# Yalnızca CSV üret
npm run migrate:onesignal -- --onesignal-app-id <ONESIGNAL_APP_ID> --onesignal-key <REST_API_KEY> --out players.csv

# Büyük uygulama: OneSignal csv_export (gz) kullan
npm run migrate:onesignal -- --onesignal-app-id <id> --onesignal-key <key> --csv-export --out players.csv

# CSV üret ve Pushizo'ya içe aktar (iş tamamlanana kadar bekler)
npm run migrate:onesignal -- --onesignal-app-id <id> --onesignal-key <key> \
  --hs-api-url https://api.pushizo.com --hs-api-key hs_live_… --hs-app-id app_…
```text

Ortam değişkenleri de kabul edilir: `ONESIGNAL_APP_ID`, `ONESIGNAL_REST_API_KEY`, `HS_API_URL`, `HS_API_KEY`, `HS_APP_ID`. Çıktı stdout'a JSON olarak yazılır: `{ players, file, importId, importStatus, stats }`.

CSV başlıkları OneSignal'in dışa aktarma biçimiyle aynıdır (`id, identifier, session_count, language, timezone, game_version, device_os, device_type, device_model, ad_id, tags, last_active, playtime, amount_spent, created_at, invalid_identifier, badge_count, external_user_id, notification_types`). OneSignal panelinden indirdiğiniz "Export" dosyasını da aracı kullanmadan doğrudan yükleyebilirsiniz.

### 3.2 CSV'yi elle içe aktarma

```bash
curl -X POST "https://api.example.com/v1/apps/app_…/imports" \
  -H "Authorization: Bearer hs_live_…" \
  -F type=onesignal_players -F file=@players.csv
# → { "id": "imp_…", "status": "queued" }
curl "https://api.example.com/v1/apps/app_…/imports/imp_…" -H "Authorization: Bearer hs_live_…"
# → { "status": "completed", "stats": { "processed", "created_users", "updated_users", "created_subscriptions", "skipped", "errors" }, "error_report_url": "…" }
```text

Dashboard eşdeğeri: **Kitle → Dışa / İçe Aktarım**. Dosya sınırı 50 MB ve 500.000 satırdır; daha büyük dosyaları bölün. İçe aktarma idempotenttir: aynı dosya ikinci kez yüklendiğinde kullanıcı ve abonelik yinelenmez.

### 3.3 Alan eşleme tablosu (`mapOneSignalRow`)

| OneSignal sütunu | Pushizo alanı | Kural |
|------------------|------------------|-------|
| `external_user_id` | `external_id` | Boşsa ve `identifier` varsa anonim kullanıcı oluşturulur; ikisi de boşsa satır hata raporuna düşer. |
| `identifier` | abonelik `token` | Push token / e-posta / telefon. E-posta küçük harfe, telefon E.164'e normalize edilir; geçersizse satır atlanır. |
| `device_type` | abonelik `channel` | `0` → `ios_push`; `1`, `2`, `13` → `android_push`; `11` → `email`; `14` → `sms`; `5`, `7`, `8`, `9`, `10` (web) → yalnızca kullanıcı oluşturulur (bkz. aşağı). Diğer değerler: abonelik yok. |
| `invalid_identifier` = `t` | abonelik `status: invalid` | |
| `notification_types` < 0 | abonelik `status: unsubscribed` | Aksi halde `subscribed`. |
| `tags` (JSON) | kullanıcı `tags` | Değerler metne çevrilir (nesne/dizi JSON metni olur, `null` atlanır). Geçersiz JSON → satır hatası. |
| `language` | `properties.language`, `device.locale` | |
| `timezone` (UTC'den saniye) | `device.timezone` | Tam saat ise `Etc/GMT∓h` (UTC+3 → `Etc/GMT-3`), değilse boş. |
| `device_model`, `device_os`, `game_version` | `device.model`, `device.os_version`, `device.app_version` | |
| `session_count`, `created_at`, `last_active` | `session_count`, `first_seen_at`, `last_active_at` | |
| `id`, `ad_id`, `playtime`, `amount_spent`, `badge_count` | — | Aktarılmaz. |

**Web push abonelikleri CSV ile taşınamaz**: OneSignal dışa aktarımı `p256dh`/`auth` anahtarlarını içermez ve VAPID anahtar çifti değiştiği için tarayıcı abonelikleri yeniden oluşturulmalıdır. Web ziyaretçileri Pushizo Web SDK'sı yüklendiğinde (`autoResubscribe: true` varsayılanıyla) izin zaten verilmişse sessizce yeniden abone olur; bu yüzden web için çift yazma dönemini uzun tutun.

### 3.4 Göç sonrası kontrol

- **Abonelikler** sayfasında kanal dağılımını OneSignal "Audience" sayılarıyla karşılaştırın.
- Bir cihazı **Test cihazı** yapıp **Mesajlar → Test push gönder** ile gerçek teslimatı doğrulayın (OneSignal'den gelen APNs/FCM token'ları Pushizo'dan de çalışır).
- `error_report_url` dosyasındaki satırları inceleyin (`line, error, row`).

## 4. API eşleme tablosu

Kimlik doğrulama: OneSignal `Authorization: Basic <REST_API_KEY>` → Pushizo `Authorization: Bearer hs_live_…`. Hata zarfı `{ "error": { "code": "HS_…", "message", "details", "request_id" } }` ([hata kataloğu](/docs/errors)). Sayfalama OneSignal'deki `offset`/`limit` yerine `cursor`/`limit` (`next_cursor`, `has_more`).

### 4.1 Uçlar

| OneSignal REST | Pushizo public API | Fark |
|----------------|----------------------|------|
| `POST /notifications` | `POST /v1/notifications` | Gövdede `app_id`; yanıt `202 { id, status, recipients_estimate }`. Idempotency için `Idempotency-Key` başlığı ya da `idempotency_key` alanı (OneSignal `external_id`). |
| `GET /notifications?app_id=` | `GET /v1/notifications?app_id=` | Cursor sayfalama, `status` filtresi. |
| `GET /notifications/{id}` | `GET /v1/notifications/{message_id}` | `counts` nesnesi (`queued, sent, delivered, failed, opened, clicked, suppressed…`). |
| `DELETE /notifications/{id}` | `DELETE /v1/notifications/{message_id}` | Yalnızca zamanlanmış/kuyruktaki mesaj iptal edilir. |
| `GET /notifications/{id}/history` | `GET /v1/notifications/{message_id}/history` | CSV işi; tamamlanınca `download_url`. |
| `POST /notifications/{id}/export_events` | `POST /v1/notifications/{message_id}/export_events` | CSV işi; `download_url`. |
| `GET /players` (view devices) | `GET /v1/apps/{app_id}/users` + `GET /v1/apps/{app_id}/subscriptions/{id}` | Cihaz kavramı yerine **kullanıcı** ve **abonelik**; aboneliğin sahibi `GET …/subscriptions/{id}/user/identity`. |
| `GET /players/{id}` | `GET /v1/apps/{app_id}/subscriptions/{subscription_id}` | Alanlar: `channel`, `status`, `token_masked`, `device`, `user_id`. |
| `POST /players` / `PUT /players/{id}` | `POST /v1/apps/{app_id}/users` (aboneliklerle) / `PATCH …/subscriptions/{id}` | Sunucu tarafı cihaz kaydı için `users.upsert` + `subscriptions[]`. |
| `PUT /players/{id}` (tag güncelleme) | `PATCH /v1/apps/{app_id}/users/by/{label}/{value}` | Merge-patch `tags`, `properties`, `deltas`. `null` değer tag'i siler. |
| `POST /players/csv_export` | `POST /v1/apps/{app_id}/exports` (`type: users` \| `subscriptions` \| `events` \| `deliveries`) | `GET …/exports/{id}` → `download_url`. |
| `GET /users/by/{alias_label}/{alias_id}` | `GET /v1/apps/{app_id}/users/by/{label}/{value}` | Aynı yol; `hs_id` OneSignal `onesignal_id`'nin karşılığıdır. |
| `POST /users` | `POST /v1/apps/{app_id}/users` | `external_id`, `tags`, `properties`, `language`, `subscriptions[]`. |
| `PATCH /users/by/{label}/{value}` | `PATCH /v1/apps/{app_id}/users/by/{label}/{value}` | |
| `DELETE /users/by/{label}/{value}` | `DELETE /v1/apps/{app_id}/users/by/{label}/{value}` | Abonelikler silinir, olaylar anonimleşir. |
| `GET/PATCH /users/by/{label}/{value}/identity` | `GET/PATCH /v1/apps/{app_id}/users/by/{label}/{value}/identity` | Alias ekleme; `DELETE …/identity/{alias_label}` kaldırır. |
| `POST /users/by/{label}/{value}/subscriptions` | `POST /v1/apps/{app_id}/users/by/{label}/{value}/subscriptions` | `{ channel, token, device? }`. |
| `PATCH /subscriptions/{id}` | `PATCH /v1/apps/{app_id}/subscriptions/{subscription_id}` | `opted_in`, `device`, `test_device`. |
| `DELETE /subscriptions/{id}` | `DELETE /v1/apps/{app_id}/subscriptions/{subscription_id}` | |
| `PATCH /subscriptions_by_token/{token_type}/{token}` | `PATCH /v1/apps/{app_id}/subscriptions_by_token/{token_type}/{token}` | OneSignal tür adları (`iOSPush`, `AndroidPush`, `ChromePush`, `FirefoxPush`, `SafariPush`, `Email`, `SMS`) da kabul edilir. |
| `POST /subscriptions/{id}/owner` | `POST /v1/apps/{app_id}/subscriptions/{subscription_id}/owner` | `{ external_id }`. |
| `GET /subscriptions/{id}/user/identity` | `GET /v1/apps/{app_id}/subscriptions/{subscription_id}/user/identity` | |
| `POST /apps/{app_id}/custom_events` | `POST /v1/apps/{app_id}/events` | `[{ external_id \| subscription_id, name, properties, value, occurred_at }]`, en fazla 500. |
| `GET /apps/{app_id}/outcomes` | `GET /v1/apps/{app_id}/analytics/conversions` | Dönüşüm olayı `event_definitions.is_conversion` ile işaretlenir (`PATCH …/events/definitions/{name}`). |
| `GET/POST /apps/{app_id}/segments`, `DELETE …/segments/{id}` | `GET/POST /v1/apps/{app_id}/segments`, `PATCH/DELETE …/{id}` | Filtre AST (`{ field, op, value }` koşulları, `all`/`any`/`none` grupları; ör. `tags.plan`, `properties.city`, `language`, `event`); `POST …/segments/preview` sayım + örnek kullanıcılar. |
| `GET/POST /templates` | `GET/POST /v1/apps/{app_id}/templates`, `PATCH/DELETE …/{id}`, `POST …/{id}/render` | Liquid; sürüm geçmişi `…/versions`, başka app'e kopya `…/copy_to_app`. |
| `GET /apps`, `GET /apps/{id}` | `GET /v1/apps`, `GET /v1/apps/{app_id}` | `PATCH /v1/apps/{app_id}` ayarları günceller. |
| `POST /apps` | `POST /v1/apps` | Org geneli `admin` anahtarı. |
| Journeys (yalnızca panel) | `GET/POST /v1/apps/{app_id}/journeys`, `…/draft`, `…/publish`, `…/runs` | API ile tasarlanabilir ve tetiklenebilir ([journey'ler](/docs/journeys)). |
| In-App Messages (yalnızca panel) | `GET/POST /v1/apps/{app_id}/in-app-messages` | Blok tabanlı; HTML yok. |
| Live Activities | — | v2 kapsamı (ADR-0017). |

Postman koleksiyonu (`api/postman_collection.json`) tüm bu uçları `{{baseUrl}}`, `{{apiKey}}`, `{{appId}}` değişkenleriyle içerir.

### 4.2 `POST /notifications` gövdesi

Hedefleme alanları Pushizo'da `target` nesnesi altında toplanır; içerik alanlarının çoğu aynı adı taşır.

| OneSignal | Pushizo | Not |
|-----------|-----------|-----|
| `included_segments` / `excluded_segments` | `target.included_segments` / `target.excluded_segments` | Segment kimlikleri (`seg_…`) ya da adları. |
| `include_aliases: { external_id: [] }` | `target.include_aliases` veya `target.include_external_ids` | En fazla 2000. |
| `include_subscription_ids` (eski `include_player_ids`) | `target.include_subscription_ids` | |
| `include_email_tokens` | `target.email_to` | Aboneliği olmayan adres için anonim kullanıcı + abonelik oluşturulur. |
| `include_phone_numbers` | `target.include_phone_numbers` | E.164. |
| `filters` | `target.filters` | Segment AST (OneSignal `field/relation/value` yerine `{ field: "tags.plan", op: "eq", value: "pro" }` ve `all`/`any`/`none` grupları). |
| `target_channel: "push" \| "email" \| "sms"` | `target.channels: ["ios_push", "android_push", "web_push", "email", "sms"]` | Verilmezse içerikten çıkarılır; SMS yalnızca açıkça. |
| `isIos`, `isAndroid`, `isAnyWeb` | `target.channels` | |
| `headings`, `contents`, `subtitle`, `url`, `app_url`, `data`, `buttons`, `big_picture`, `ios_attachments`, `ios_sound`, `ios_category`, `thread_id`, `ios_relevance_score`, `target_content_identifier`, `android_channel_id`, `android_group`, `android_sound`, `small_icon`, `large_icon`, `android_accent_color`, `android_visibility`, `chrome_web_icon`, `chrome_web_image`, `chrome_web_badge`, `web_push_topic`, `web_buttons`, `content_available`, `mutable_content`, `template_id`, `custom_data`, `send_after`, `delayed_option`, `delivery_time_of_day`, `ttl`, `priority`, `collapse_id`, `email_subject`, `email_body`, `email_preheader`, `email_from_name`, `email_from_address`, `email_reply_to_address`, `disable_email_click_tracking`, `sms_from`, `sms_media_urls`, `include_unsubscribed`, `name` | aynı ad | `priority` OneSignal'deki gibi `5` (normal) ya da `10` (yüksek); `ttl` saniye. |
| `ios_badgeType`, `ios_badgeCount` | `ios_badge_type`, `ios_badge_count` | snake_case. |
| `ios_interruption_level` | `ios_interruption_level` | `passive`, `active`, `time-sensitive`, `critical` ([critical alert rehberi](/docs/channels/ios-critical-alerts)). |
| `external_id` (idempotency) | `idempotency_key` ya da `Idempotency-Key` başlığı | Aynı anahtar + aynı gövde aynı yanıtı döndürür; farklı gövde `409 HS_IDEMPOTENCY_CONFLICT`. |
| `send_after` + `delayed_option: "timezone"` | aynı | `delivery_time_of_day: "09:00"`. |
| `throttle_rate_per_minute` | `throttle_rate_per_minute` | |
| `email_mjml` | `email_mjml` | OneSignal'de yok; MJML derlemesi Pushizo'ya özgüdür. |
| — | `add_to_inbox`, `inbox_ttl`, `category`, `labels`, `one_per_user`, `frequency_cap_exempt`, `quiet_hours_exempt`, `require_interaction` | Pushizo ek alanları. |

Örnek (Node sunucu SDK'sı):

```ts
import { Pushizo } from '@pushizo/node';
const hs = new Pushizo({ apiKey: process.env.HS_API_KEY!, appId: process.env.HS_APP_ID, baseUrl: 'https://api.example.com' });

await hs.notifications.create({
  idempotency_key: `order-${order.id}`,
  target: { include_aliases: { external_id: [userId] } },
  headings: { tr: 'Siparişiniz yola çıktı', en: 'Your order has shipped' },
  contents: { tr: 'Kargo takip bağlantısı içeride', en: 'Tracking link inside' },
  url: `https://example.com/orders/${order.id}`,
  data: { order_id: order.id },
});
```text

### 4.3 Webhook'lar

OneSignal "Event Streams" yerine Pushizo **webhook endpoint'leri** (`/v1/apps/{app_id}/webhooks`) HMAC imzalı (`Pushizo-Signature: t=…,v1=…`) olaylar gönderir: `delivery.*`, `subscription.*`, `user.*`, `event.received`, `conversion.created`, `campaign.*`, `journey.*`, `in_app.*`. İmza doğrulama ve yeniden deneme planı için [webhook'lar](/docs/webhooks).

## 5. SDK değişim rehberi

Tüm istemci SDK'larında gizli anahtar yoktur; yalnızca **App ID** (ve kendi API adresiniz) kullanılır. `identity verification` zorunluysa `login` ikinci parametre olarak sunucunuzun ürettiği `identity_hash = HMAC-SHA256(sdk_secret, external_id)` değerini alır (OneSignal'deki JWT yerine).

### 5.1 Web

| OneSignal v16 (Web) | Pushizo Web SDK (`@pushizo/web`) |
|---------------------|-----------------------------------------|
| `<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js">` + `OneSignalDeferred.push(async (OneSignal) => { await OneSignal.init({ appId }) })` | `<script src="https://api.example.com/sdk-web/PushizoSDK.js" async>` + `PushizoDeferred.push(async (Pushizo) => { await Pushizo.init({ appId: 'app_…', apiUrl: 'https://api.example.com' }) })` |
| `OneSignalSDKWorker.js` site kökünde | `PushizoSDKWorker.js` site kökünde (`importScripts(apiUrl + '/sdk-web/PushizoSDKWorker.js')`); `serviceWorkerPath` / `serviceWorkerScope` seçenekleri aynı anlamda |
| `OneSignal.login(externalId, jwtToken?)` | `Pushizo.login(externalId, identityHash?)` |
| `OneSignal.logout()` | `Pushizo.logout()` |
| `OneSignal.User.addTag / addTags / removeTag / removeTags / getTags` | aynı adlar: `Pushizo.User.addTag / addTags / removeTag / removeTags / getTags` |
| `OneSignal.User.addAlias / addAliases / removeAlias` | aynı adlar |
| `OneSignal.User.addEmail / removeEmail / addSms / removeSms` | aynı adlar (adres istemcide normalize edilir) |
| `OneSignal.User.setLanguage` | `Pushizo.User.setLanguage` |
| `OneSignal.User.PushSubscription.optIn / optOut / id / token / optedIn` + `addEventListener('change')` | `Pushizo.User.pushSubscription.optIn / optOut / id / optedIn / state` + `addObserver(cb)` (geri dönen fonksiyon dinleyiciyi kaldırır) |
| `OneSignal.Notifications.requestPermission()` / `permission` | `Pushizo.Notifications.requestPermission()` / `permission` (`'default' \| 'granted' \| 'denied' \| 'unsupported'`) |
| `OneSignal.Notifications.addEventListener('click', cb)` | `Pushizo.Notifications.addClickListener(({ notification, result }) => …)` |
| `OneSignal.Notifications.addEventListener('foregroundWillDisplay', cb)` | `Pushizo.Notifications.addForegroundLifecycleListener((e) => e.preventDefault())` |
| `OneSignal.Slidedown.promptPush / promptEmail / promptSms / promptPushCategories` | `Pushizo.Slidedown.promptPush / promptEmail / promptSms / promptEmailAndSms` (kategori tercihleri için `User.setPreferences`) |
| `promptOptions.slidedown`, `welcomeNotification`, `notificationClickHandlerMatch/Action`, `notifyButton` | `slidedown`, `welcomeNotification`, `notificationClickHandlerMatch` / `notificationClickHandlerAction`, `bell` (init seçenekleri) |
| `OneSignal.Session.sendOutcome / sendUniqueOutcome / sendOutcomeWithValue` | `Pushizo.Session.sendEvent(name, properties?, value?)` |
| `OneSignal.User.PushSubscription` bildirim kutusu yok | `Pushizo.Inbox.list / markRead / markAllRead / archive / unreadCount / addObserver` |
| In-app (yalnızca mobil) | `Pushizo.InAppMessages.addTrigger / addTriggers / removeTrigger / clearTriggers / setScreen / paused / addClickListener / addLifecycleListener` (web dahil) |
| `OneSignal.setConsentRequired(true)` + `setConsentGiven(true)` | `init({ requiresPrivacyConsent: true })` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Debug.setLogLevel` | `Pushizo.Debug.setLogLevel('debug')` |

Ayrıntı: [Web SDK](/docs/sdk/web), çatı entegrasyonları: [React](/docs/integrations/react), [Vue](/docs/integrations/vue), [Angular](/docs/integrations/angular), iOS Safari PWA: [iOS web push](/docs/channels/ios-pwa-web-push).

### 5.2 iOS

| OneSignal iOS SDK 5 | Pushizo iOS SDK (`Pushizo`, `PushizoExtension`) |
|---------------------|----------------------------------------------------------|
| `OneSignal.initialize("APP_ID", withLaunchOptions:)` | `Pushizo.initialize("app_…", launchOptions:) { options in options.apiUrl = "https://api.example.com" }` |
| Swizzling ile token | Swizzling yok: `didRegisterForRemoteNotificationsWithDeviceToken` içinde `Pushizo.setDeviceToken(deviceToken)`, hata durumunda `Pushizo.setDeviceTokenError(error)` |
| `OneSignal.login("id")` / `logout()` | `Pushizo.login("id", identityHash: nil)` / `Pushizo.logout()` |
| `OneSignal.User.addTag/addTags/removeTag/getTags/setLanguage` | aynı adlar |
| `OneSignal.User.addEmail/addSms/removeEmail/removeSms` | aynı adlar |
| `OneSignal.User.pushSubscription.optIn()/optOut()/id/token/optedIn` + `addObserver` | `Pushizo.User.pushSubscription.optIn()/optOut()/id/token`, `await state()`, `addObserver { state in }` |
| `OneSignal.Notifications.requestPermission(_:fallbackToSettings:)` | `Pushizo.Notifications.requestPermission { granted in }` (`await permission()` ile okuma) |
| `OneSignal.Notifications.addPermissionObserver` | `addPermissionObserver { granted in }` → `HSListenerToken`; `removePermissionObserver(token)` |
| `addClickListener` / `addForegroundLifecycleListener` | aynı adlar; belirteç döndürür (`removeClickListener(token)`) |
| `OneSignalNotificationServiceExtension` | `PushizoNotificationServiceExtension(appId:apiUrl:appGroup:)` → `didReceive`, `serviceExtensionTimeWillExpire` |
| `OneSignal.InAppMessages.addTrigger/removeTrigger/clearTriggers/paused` | aynı adlar + `setScreen`, `addClickListener`, `addLifecycleListener` |
| `OneSignal.Session.sendOutcome(_:)` | `Pushizo.Session.sendEvent("name", properties:, value:)` |
| `OneSignal.setConsentRequired/setConsentGiven` | `options.requiresPrivacyConsent = true` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Notifications.clearAll()` | `Pushizo.Notifications.clearAll()`, `removeGroupedNotifications`, `removeNotification(identifier:)` |
| Live Activities | v2 |

Aynı APNs `.p8` anahtarı kullanıldığından mevcut cihaz token'ları Pushizo'dan de teslim alır; SDK ilk açılışta token'ı `POST /subscriptions` ile kaydeder (göçte aktarılmış abonelikle `(app, channel, token)` üzerinden birleşir). Ayrıntı: [iOS SDK](/docs/sdk/ios).

### 5.3 Android

| OneSignal Android SDK 5 | Pushizo Android SDK (`com.pushizo:pushizo-android`) |
|-------------------------|---------------------------------------------------------------|
| `OneSignal.initWithContext(this, "APP_ID")` | `Pushizo.initialize(this, "app_…", Pushizo.Options(apiUrl = "https://api.example.com"))` (Application `onCreate`) |
| `OneSignal.login("id")` / `logout()` | `Pushizo.login("id", identityHash = null)` / `Pushizo.logout()` |
| `OneSignal.User.addTag/addTags/removeTag/getTags/setLanguage` | aynı adlar |
| `OneSignal.User.addEmail/addSms/removeEmail/removeSms` | aynı adlar (`emails`, `smsNumbers` listeleri) |
| `OneSignal.User.pushSubscription.optIn()/optOut()/id/token/optedIn` + `addObserver` | aynı adlar (`state` ek) |
| `OneSignal.Notifications.requestPermission(fallbackToSettings)` (coroutine) | `Pushizo.Notifications.requestPermission(activity)` + `onRequestPermissionsResult` içinde `Pushizo.Notifications.onPermissionResult()` (`PERMISSION_REQUEST_CODE`) |
| `addPermissionObserver` | `addPermissionObserver(observer)` / `removePermissionObserver(observer)` |
| `addClickListener` / `addForegroundLifecycleListener` | aynı adlar |
| `OneSignal.InAppMessages.addTrigger/…/paused` | aynı adlar + `setScreen`, `addClickListener`, `addLifecycleListener` |
| `OneSignal.Session.sendOutcome` | `Pushizo.Session.sendEvent("name", mapOf(...), value)` |
| `OneSignal.consentRequired = true` / `consentGiven = true` | `Options(requiresPrivacyConsent = true)` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Notifications.clearAllNotifications()` | `Pushizo.Notifications.clearAll()`, `removeNotification`, `removeGroupedNotifications` |
| Bildirim kanalları (panelden) | Dashboard → Ayarlar → Android bildirim kategorileri (`android_channels`); `android_channel_id` ile seçilir |

Aynı Firebase projesi kullanıldığı sürece FCM token'ları değişmez. Ayrıntı: [Android SDK](/docs/sdk/android).

### 5.4 Flutter

| `onesignal_flutter` | `pushizo_flutter` |
|---------------------|----------------------|
| `OneSignal.initialize("APP_ID")` | `await Pushizo.initialize('app_…', apiUrl: 'https://api.example.com')` |
| `OneSignal.login/logout` | `Pushizo.login(externalId, identityHash: null)` / `Pushizo.logout()` |
| `OneSignal.User.addTags/removeTag/getTags/setLanguage` | aynı adlar (hepsi `Future`) |
| `OneSignal.User.addEmail/addSms/removeEmail/removeSms` | aynı adlar |
| `OneSignal.User.pushSubscription.optIn/optOut/id/token/optedIn` + `addObserver` | `Pushizo.User.pushSubscription.optIn()/optOut()`, `await state`, `changes.listen(...)` |
| `OneSignal.Notifications.requestPermission(true)` | `await Pushizo.Notifications.requestPermission()` |
| `OneSignal.Notifications.addPermissionObserver` | `Pushizo.Notifications.addPermissionObserver(fn)` ya da `permissionStream.listen(...)` |
| `addClickListener` / `addForegroundWillDisplayListener` | `Pushizo.Notifications.clickStream.listen(...)` / `foregroundStream.listen(...)` |
| `OneSignal.InAppMessages.addTrigger/…/paused(bool)` | aynı adlar + `setScreen`, `setPaused(bool)`, `clickStream`, `lifecycleStream`, `setNavigatorKey` |
| `OneSignal.Session.sendOutcome` | `Pushizo.Session.sendEvent(name, properties:, value:)` |
| `OneSignal.consentRequired(true)` / `consentGiven(true)` | `initialize(..., requiresPrivacyConsent: true)` + `Pushizo.setConsentGiven(true)` |
| — | `Pushizo.Inbox.list/markRead/markAllRead/archive/unreadCount`, `Pushizo.User.getPreferences/setPreferences` |

Ayrıntı: [Flutter SDK](/docs/sdk/flutter).

### 5.5 Köprü SDK'ları için ortak notlar

Aşağıdaki tablolarda OneSignal sütunu ilgili paketin **v5** API adlarını esas alır; daha eski (v3/v4) bir OneSignal sürümünden geçiyorsanız önce çağrıları v5 karşılıklarına eşleyin. Tüm köprü SDK'larında:

- Paket değişimi tek seferdedir: OneSignal paketini (ve Expo için `onesignal-expo-plugin`) kaldırıp Pushizo paketini ekleyin, yerel projeyi yeniden üretin (`pod install`, `npx cap sync`, `cordova prepare`, `expo prebuild`). Pushizo iOS'ta `UNUserNotificationCenter` delegesini üstlendiğinden aynı sürümde iki push SDK'sı birlikte bulunmamalıdır.
- `initialize` App ID'nin yanında kendi API adresinizi (`apiUrl`) alır; REST API anahtarı istemciye gömülmez.
- OneSignal "outcome" çağrıları (`addOutcome`, `addUniqueOutcome`, `addOutcomeWithValue` ve C# karşılıkları) tek bir `Session.sendEvent(name, properties?, value?)` çağrısına dönüşür; aynı ad in-app `event` tetiklerini de çalıştırır. Tekillik (unique) gerekiyorsa uygulamada ya da journey/segment koşulunda sağlayın.
- Bildirim kutusu (`Inbox`) ve kategori tercihleri (`User.getPreferences` / `setPreferences`) OneSignal'de karşılığı olmayan eklemelerdir (bkz. 5.11).

### 5.6 React Native

| `react-native-onesignal` 5 | `@pushizo/react-native` |
|----------------------------|----------------------------|
| Expo: `["onesignal-expo-plugin", { "mode": "development" }]` | `["@pushizo/react-native", { "mode": "development", "appId": "app_…" }]` (plugin pakete dahildir; NSE hedefi `PushizoNotificationServiceExtension`) |
| Bare iOS: token swizzling ile alınır | Swizzling yok: `AppDelegate` içinde `Pushizo.setDeviceToken(deviceToken)` / `Pushizo.setDeviceTokenError(error)` (Expo plugin otomatik ekler) |
| `OneSignal.initialize('APP_ID')` | `await Pushizo.initialize('app_…', { apiUrl: 'https://api.example.com' })` |
| `OneSignal.login(externalId)` / `OneSignal.logout()` | `Pushizo.login(externalId, identityHash?)` / `Pushizo.logout()` |
| `OneSignal.User.addTag / addTags / removeTag / removeTags / getTags` | aynı adlar |
| `OneSignal.User.addAlias / addAliases / removeAlias` | aynı adlar |
| `OneSignal.User.addEmail / removeEmail / addSms / removeSms`, `setLanguage` | aynı adlar |
| `OneSignal.User.getExternalId()` / `getOnesignalId()` | `Pushizo.User.getExternalId()` / `getPushizoId()` |
| `OneSignal.User.pushSubscription.getIdAsync / getTokenAsync / getOptedInAsync / optIn / optOut` + `addEventListener('change', …)` | aynı adlar (`{ previous, current }`) |
| `OneSignal.Notifications.requestPermission(fallbackToSettings)` | `Pushizo.Notifications.requestPermission()` |
| `OneSignal.Notifications.getPermissionAsync()` | `Pushizo.Notifications.getPermissionAsync()` (eşzamanlı son değer: `permission`) |
| `OneSignal.Notifications.addEventListener('click' \| 'foregroundWillDisplay' \| 'permissionChange', …)` | aynı olay adları; `e.preventDefault()` ve `e.getNotification().display()` aynı |
| `OneSignal.Notifications.clearAll()` | `Pushizo.Notifications.clearAll()` |
| `OneSignal.InAppMessages.addTrigger / addTriggers / removeTrigger / removeTriggers / clearTriggers / setPaused / getPaused` | aynı adlar + `getTriggers`, `setScreen` |
| `OneSignal.InAppMessages.addEventListener('click' \| 'willDisplay' \| 'didDisplay' \| 'willDismiss' \| 'didDismiss', …)` | aynı olay adları; olay gövdesi `{ messageId, actionId, url }` / `{ messageId }` |
| `OneSignal.Session.addOutcome / addUniqueOutcome / addOutcomeWithValue` | `Pushizo.Session.sendEvent(name, properties?, value?)` |
| `OneSignal.setConsentRequired(true)` + `setConsentGiven(true)` | `initialize(…, { requiresPrivacyConsent: true })` + `Pushizo.setConsentGiven(true)` |
| `OneSignal.Debug.setLogLevel(LogLevel.Verbose)` | `Pushizo.Debug.setLogLevel(LogLevel.Debug)` |
| — | `Pushizo.Inbox.*`, `Pushizo.User.getPreferences / setPreferences` |

Ayrıntı: [React Native SDK](/docs/sdk/react-native).

### 5.7 Capacitor (Ionic)

OneSignal Ionic/Capacitor uygulamalarında `onesignal-cordova-plugin` paketini kullanır; Pushizo'da ayrı bir Capacitor eklentisi vardır (web'de `@pushizo/web`'e devreder).

| `onesignal-cordova-plugin` 5 (Capacitor) | `@pushizo/capacitor` |
|------------------------------------------|-------------------------|
| `npm install onesignal-cordova-plugin` | `npm install @pushizo/capacitor` (+ web için `@pushizo/web`), `npx cap sync` |
| iOS token swizzling ile alınır | `AppDelegate.swift` içinde `capacitorDidRegisterForRemoteNotifications` / `capacitorDidFailToRegisterForRemoteNotifications` bildirimlerini gönderin |
| `OneSignal.initialize('APP_ID')` | `await Pushizo.initialize('app_…', { apiUrl: 'https://api.example.com' })` |
| `OneSignal.login / logout` | `Pushizo.login(externalId, identityHash?)` / `Pushizo.logout()` (Promise) |
| `OneSignal.User.addTag / addTags / removeTag / removeTags / getTags`, `addAlias / addAliases / removeAlias`, `addEmail / removeEmail / addSms / removeSms`, `setLanguage` | aynı adlar (hepsi Promise) |
| `OneSignal.User.pushSubscription.getIdAsync / getTokenAsync / getOptedInAsync / optIn / optOut` | `Pushizo.User.pushSubscription.getId / getToken / getOptedIn / getState / optIn / optOut` |
| `OneSignal.User.pushSubscription.addEventListener('change', cb)` | `Pushizo.User.pushSubscription.addObserver((state) => …)` (dönen fonksiyon aboneliği kaldırır) |
| `OneSignal.Notifications.requestPermission(true)` | `await Pushizo.Notifications.requestPermission()` |
| `OneSignal.Notifications.addEventListener('click', cb)` | `Pushizo.Notifications.addClickListener(({ notification, result }) => …)` |
| `OneSignal.Notifications.addEventListener('foregroundWillDisplay', cb)` | `Pushizo.Notifications.addForegroundLifecycleListener((e) => e.preventDefault())`; erteleyip göstermek için `await e.display()` |
| `OneSignal.Notifications.addEventListener('permissionChange', cb)` | `Pushizo.Notifications.addPermissionObserver(({ permission }) => …)` |
| `OneSignal.Notifications.clearAll()` | `Pushizo.Notifications.clearAll()`, `removeGroupedNotifications`, `removeNotification` (yalnızca Android) |
| `OneSignal.InAppMessages.addTrigger / addTriggers / removeTrigger / removeTriggers / clearTriggers / setPaused / getPaused` | aynı adlar + `getTriggers`, `setScreen` |
| `OneSignal.InAppMessages.addEventListener('click' \| 'willDisplay' \| …, cb)` | `Pushizo.InAppMessages.addClickListener(cb)` / `addLifecycleListener({ willDisplay, didDisplay, willDismiss, didDismiss })` |
| `OneSignal.Session.addOutcome / addUniqueOutcome / addOutcomeWithValue` | `Pushizo.Session.sendEvent(name, properties?, value?)` |
| `OneSignal.setConsentRequired(true)` + `setConsentGiven(true)` | `initialize(…, { requiresPrivacyConsent: true })` + `await Pushizo.setConsentGiven(true)` |
| `OneSignal.Debug.setLogLevel(…)` | `await Pushizo.Debug.setLogLevel('debug')` |

Ayrıntı: [Capacitor SDK](/docs/sdk/capacitor).

### 5.8 Cordova

| `onesignal-cordova-plugin` 5 | `cordova-plugin-pushizo` |
|------------------------------|-----------------------------|
| `cordova plugin add onesignal-cordova-plugin`, `window.plugins.OneSignal` | `cordova plugin add cordova-plugin-pushizo`, `window.Pushizo` (ve `window.plugins.Pushizo`) |
| Android Gradle ayarı gerekmez | `config.xml` → `GradlePluginKotlinEnabled` ve `GradlePluginGoogleServicesEnabled` (köprü Kotlin'dir) |
| `OneSignal.initialize('APP_ID')` (`deviceready` içinde) | `await Pushizo.initialize('app_…', { apiUrl: 'https://api.example.com' })` |
| Diğer tüm çağrılar | Capacitor tablosuyla (5.7) birebir aynı; tüm metotlar Promise döndürür, hatalar `PushizoError` (`code`) |

iOS'ta APNs token geri çağrıları eklenti tarafından AppDelegate'e çalışma zamanında eklenir; ek kod gerekmez. Ayrıntı: [Cordova SDK](/docs/sdk/cordova).

### 5.9 .NET MAUI

| `OneSignalSDK.DotNet` 5 | `Pushizo.Maui` |
|-------------------------|-------------------|
| `dotnet add package OneSignalSDK.DotNet`, `using OneSignalSDK.DotNet;` | `dotnet add package Pushizo.Maui`, `using PushizoSdk.Maui;` |
| `OneSignal.Initialize("APP_ID")` | `builder.UsePushizo("app_…", o => o.ApiUrl = "https://api.example.com")` (`MauiProgram.cs`) ya da `Pushizo.Initialize("app_…", options)` |
| iOS token swizzling ile alınır | `AppDelegate` içinde `PushizoIos.RegisteredForRemoteNotifications(deviceToken)` / `FailedToRegisterForRemoteNotifications(error)` |
| `OneSignal.Login("id")` / `OneSignal.Logout()` | `Pushizo.Login("id", identityHash)` / `Pushizo.Logout()` |
| `OneSignal.User.AddTag / AddTags / RemoveTag / RemoveTags`, `AddAlias / AddAliases / RemoveAlias`, `AddEmail / RemoveEmail / AddSms / RemoveSms` | aynı adlar + `GetTags()`, `SetLanguage("tr")` |
| `OneSignal.User.PushSubscription.OptIn() / OptOut() / Id / Token / OptedIn` + `Changed` | aynı adlar (`e.Previous`, `e.Current`) |
| `await OneSignal.Notifications.RequestPermissionAsync(true)` | `await Pushizo.Notifications.RequestPermissionAsync(fallbackToSettings: true)` |
| `OneSignal.Notifications.Clicked / ForegroundWillDisplay / PermissionChanged` | aynı olay adları (`EventHandler`); tıklamada `e.Notification`, `e.ActionId`, `e.Url`; ön planda `e.PreventDefault()` |
| `OneSignal.Notifications.ClearAllNotifications()` | `Pushizo.Notifications.ClearAll()` |
| `OneSignal.InAppMessages.AddTrigger / AddTriggers / RemoveTrigger / RemoveTriggers / ClearTriggers / Paused` + `Clicked / WillDisplay / DidDisplay / WillDismiss / DidDismiss` | aynı adlar + `GetTriggers()`, `SetScreen(name)` |
| `OneSignal.Session.AddOutcome / AddUniqueOutcome / AddOutcomeWithValue` | `Pushizo.Session.SendEvent(name, properties, value)` |
| `OneSignal.ConsentRequired = true` + `OneSignal.ConsentGiven = true` | `o.RequiresPrivacyConsent = true` + `Pushizo.SetConsentGiven(true)` |
| `OneSignal.Debug.LogLevel = …` | `Pushizo.Debug.SetLogLevel(LogLevel.Debug)` |
| — | `Pushizo.Inbox.ListAsync / MarkReadAsync / MarkAllReadAsync / ArchiveAsync / UnreadCountAsync`, `Pushizo.User.GetPreferencesAsync / SetPreferencesAsync` |

Ayrıntı: [.NET MAUI SDK](/docs/sdk/maui).

### 5.10 Unity

| OneSignal Unity SDK 5 | `com.pushizo.unity` |
|-----------------------|------------------------|
| OneSignal UPM paketleri, `using OneSignalSDK;` | Git URL ile UPM paketi (`https://github.com/exec0de/pushizo.git?path=sdk/unity#sdk-v0.1.0`), `using PushizoSdk;`; Project Settings → **Pushizo** (App ID, App Group, `google-services.json` yolu) |
| `OneSignal.Initialize("APP_ID")` | `Pushizo.Initialize("app_…", new PushizoOptions { ApiUrl = "https://api.example.com" })` |
| `OneSignal.Login("id")` / `OneSignal.Logout()` | `Pushizo.Login("id", identityHash)` / `Pushizo.Logout()` |
| `OneSignal.User.AddTag / AddTags / RemoveTag / RemoveTags`, `AddAlias / AddAliases / RemoveAlias`, `AddEmail / RemoveEmail / AddSms / RemoveSms` | aynı adlar + `GetTags()`, `SetLanguage("tr")` |
| `OneSignal.User.PushSubscription.OptIn() / OptOut() / Id / Token / OptedIn` + `Changed` | aynı adlar |
| `await OneSignal.Notifications.RequestPermissionAsync(true)` | `await Pushizo.Notifications.RequestPermissionAsync(fallbackToSettings: true)` |
| `OneSignal.Notifications.Clicked / ForegroundWillDisplay / PermissionChanged += (sender, e) => …` | aynı olay adları, `Action<T>` imzası: `Clicked += e => …`, `PermissionChanged += granted => …` (olaylar ana iş parçacığında gelir) |
| `OneSignal.Notifications.ClearAllNotifications()` | `Pushizo.Notifications.ClearAll()` |
| `OneSignal.InAppMessages.AddTrigger / AddTriggers / RemoveTrigger / RemoveTriggers / ClearTriggers / Paused` + `Clicked / WillDisplay / DidDisplay / WillDismiss / DidDismiss` | aynı adlar + `GetTriggers()`, `SetScreen(name)` |
| `OneSignal.Session.AddOutcome / AddUniqueOutcome / AddOutcomeWithValue` | `Pushizo.Session.SendEvent(name, properties, value)` |
| `OneSignal.ConsentRequired = true` + `OneSignal.ConsentGiven = true` | `new PushizoOptions { RequiresPrivacyConsent = true }` + `Pushizo.SetConsentGiven(true)` |
| `OneSignal.Debug.LogLevel = …` | `Pushizo.Debug.SetLogLevel(LogLevel.Debug)` |
| — | `Pushizo.Inbox.*Async`, `Pushizo.User.GetPreferencesAsync / SetPreferencesAsync` |

Android bağımlılıkları EDM4U ile çözülür ve `Assets/google-services.json` derlemede işlenir (Firebase Unity SDK'sı gerekmez); iOS'ta Push yeteneği, NSE hedefi ve Podfile derleme sonrası işlemle eklenir. Ayrıntı: [Unity SDK](/docs/sdk/unity).

### 5.11 Bildirim kutusu ve tercihler (OneSignal'de olmayanlar)

- **Bildirim kutusu:** `add_to_inbox: true` ile gönderilen mesajlar kullanıcıya özel kutuda listelenir (`Inbox.list`, `markRead`, `markAllRead`, `archive`, `unreadCount`); OneSignal'de karşılığı olmadığından taşınacak veri yoktur.
- **Kategori tercihleri:** OneSignal "push categories" slidedown'ı yerine dashboard'da **Kategoriler** tanımlanır; SDK'lar `User.getPreferences()` / `setPreferences([...])` ile kullanıcı × kanal opt-in yazar. OneSignal'de tag ile yönettiğiniz tercihleri (`tags.promo_optin = "0"`) göç sonrasında bir kerelik `PUT /v1/apps/{app_id}/users/by/external_id/{id}/preferences` çağrısıyla gerçek tercihe çevirebilirsiniz.

## 6. Kesinti planı (çift yazma dönemi)

1. **Hafta 0 — hazırlık:** kanallar doğrulanmış, player göçü tamamlanmış, sunucu entegrasyonu Pushizo'ya de yazıyor (çift yazma: her bildirim/olay OneSignal ve Pushizo'ya birlikte gider; Pushizo'da `test_devices_only: true` ile yalnızca test cihazlarına gönderim yapın ya da hedefi küçük bir segmentle sınırlayın).
2. **Hafta 1–2 — istemci yayını:** Pushizo SDK'sını içeren mobil sürümü yayınlayın, web'de SDK'yı canlıya alın. Her iki SDK aynı uygulamada çalışabilir; ancak **push izni istemini yalnızca Pushizo'dan** yapın (iki SDK birden izin isterse kullanıcı kafa karışıklığı yaşar) ve Android'de yalnızca birinin ön plan bildirimi göstermesine izin verin (`addForegroundLifecycleListener` + `preventDefault()`).
3. **Hafta 2–3 — trafik kaydırma:** işlemsel bildirimleri (sipariş, OTP) Pushizo'dan, kampanyaları dashboard'dan göndermeye başlayın. OneSignal'den gönderimi segment bazında azaltın; **Analitik → Teslimat raporu** ve **Sağlayıcı sağlığı** sayfalarından teslim/açılma oranlarını karşılaştırın.
4. **Hafta 3–4 — kesinti:** OneSignal gönderimlerini durdurun, `migrate:onesignal` ile son bir fark çekimi yapın (içe aktarma idempotenttir; yeni player'lar eklenir, mevcutlar güncellenir), OneSignal webhook/Event Stream'lerini kapatın, REST API anahtarını iptal edin.
5. **Eski uygulama sürümleri:** OneSignal SDK'sı içeren eski sürümler hâlâ OneSignal'e token kaydeder; bu kullanıcılar için haftalık fark çekimini sürümün kullanım payı %1'in altına inene kadar sürdürün.
6. **Geri alma:** çift yazma döneminde OneSignal yapılandırmasını silmeyin; kritik bir sorun olursa gönderimi OneSignal'e geri alabilirsiniz. Kesintiden sonra `.p8`/servis hesabı anahtarlarını OneSignal'den kaldırın.

## 7. Sık karşılaşılan farklar

| Konu | OneSignal | Pushizo |
|------|-----------|------------|
| Kimlik | `onesignal_id` + `external_id` | `hs_id` + `external_id` + özel alias'lar (`users/by/{label}/{value}`) |
| Cihaz birleştirme | `login` cihazı kullanıcıya bağlar | Aynı; anonim kullanıcı birleştirilir, `user.merged` webhook'u üretilir |
| Hata biçimi | `{ "errors": [...] }` | `{ "error": { "code": "HS_…", … } }`, `x-request-id` |
| Hız sınırı | sabit | Plan bazlı (`api_rps`), `X-RateLimit-*` başlıkları, `429 HS_RATE_LIMITED` + `Retry-After` |
| Sessiz saatler / frekans sınırı | panel | App ayarları; mesaj başına `quiet_hours_exempt`, `frequency_cap_exempt` |
| Şablon dili | `{{ tag }}` basit değişkenler | Liquid (`{{ user.tags.name }}`, filtreler, koşullar) |
| Web push kimliği | OneSignal VAPID | Kendi VAPID çiftiniz (kanal kaydında üretilir); abonelikler yeniden oluşur |
| Veri yerleşimi | OneSignal bulutu | Kendi altyapınız (self-hosted) |

Bu sayfada