Pushizo Docs

E-posta, SMS, in-app mesajlar ve bildirim kutusu

Faz 10 ile Pushizo push dışındaki kanalları da gönderir: e-posta (SMTP, Amazon SES, Resend), SMS (Twilio, Netgsm), uygulama içi in-app mesajlar ve bildirim kutusu (inbox). Bu belge kurulumu, API sözleşmesini ve…

Faz 10 ile Pushizo push dışındaki kanalları da gönderir: e-posta (SMTP, Amazon SES, Resend), SMS (Twilio, Netgsm), uygulama içi in-app mesajlar ve bildirim kutusu (inbox). Bu belge kurulumu, API sözleşmesini ve davranışları özetler. Mimari kararlar: docs/adr/0015-email-sms-inapp.md.

1. Kanal kurulumu

Dashboard App ayarları → Kanallar → E-posta / SMS ya da PUT /internal/v1/apps/{app_id}/channels/{email|sms} (public: PUT /v1/apps/{app_id}/channels/{channel}, admin). Gerçek sağlayıcılar kayıttan sonra unconfigured olur; Doğrula (POST …/channels/{channel}/validate) sağlayıcıya test bağlantısı yapar ve başarılıysa active yapar. Gizli alanlar (parola, API anahtarı, auth token) envelope encryption ile saklanır, hiçbir yanıtta dönmez; güncellemede boş bırakılırsa kayıtlı değer korunur.

SağlayıcıKanalAlanlar (gizli olanlar italik)Doğrulama
smtpemailhost, port, secure (465 TLS / STARTTLS), username, password, gönderici alanlarıSMTP EHLO+kimlik (verify)
sesemailregion, access_key_id, secret_access_key, configuration_set, gönderici alanlarıGET /v2/email/account (SigV4)
resendemailapi_key, webhook_secret (whsec_…, isteğe bağlı), gönderici alanlarıGET /domains (yalnızca gönderim yetkili anahtar da geçerli)
twiliosmsaccount_sid, auth_token, from_number (E.164 veya alfanümerik) ya da messaging_service_sidGET /Accounts/{sid}.json
netgsmsmsusercode, password, msgheader, iys_filter (0/11/12)GET /sms/rest/v2/msgheader + başlık kontrolü
mockemail/smse-postada isteğe bağlı from_address, from_nameher zaman geçerli

Gönderici alanları (e-posta): from_address (zorunlu), from_name, reply_to, postal_address (altbilgide gösterilen fiziksel adres). Mesaj içeriğindeki email_from_address / email_from_name / email_reply_to_address kanal varsayılanını ezer.

Alan adı doğrulaması (DKIM/SPF/DMARC): gönderici alan adınızı sağlayıcıda doğrulayın; SPF kaydına sağlayıcının include: değerini, DKIM için sağlayıcının verdiği CNAME/TXT kayıtlarını ekleyin ve en az v=DMARC1; p=none; rua=mailto:… ile DMARC yayımlayın. Doğrulanmamış alan adından gönderim SES/Resend tarafından reddedilir (auth → kanal invalid).

2. Mesaj içeriği

POST /v1/notifications, kampanya ve şablon içeriği aynı şemayı kullanır. Hedefte target.channels: ["email"] / ["sms"] ile kanal seçilir; birden çok kanal birlikte hedeflenebilir (her abonelik kendi kanalının alanlarıyla render edilir).

AlanAçıklama
email_subjectKonu (Liquid). Yoksa headings kullanılır.
email_mjmlMJML kaynağı (Liquid → MJML derlemesi). mj-include dosya erişimi kapalıdır.
email_bodyHazır HTML (Liquid). MJML yoksa kullanılır.
email_preheaderGelen kutusu önizleme metni (gizli ön başlık).
email_from_name, email_from_address, email_reply_to_addressKanal varsayılanlarını ezer.
disable_email_click_trackingBağlantılar yönlendirmeye çevrilmez.
contentsSMS gövdesi (dil seçimi + Liquid); e-postada MJML/HTML yoksa sade şablonla gövde olur.
sms_fromGönderici (numara/başlık) ezmesi.
sms_media_urlsMMS medya adresleri (en fazla 10, https://). Yalnızca Twilio; SMS kanalı Netgsm ise oluşturma 422, gönderimde MMS_NOT_SUPPORTED.
disable_sms_link_shorteningSMS'teki bağlantılar kısa linke çevrilmez.
add_to_inbox, inbox_ttlHedef kullanıcıların bildirim kutusuna da ekler (saniye cinsinden geçerlilik).

Doğrudan adresler (OneSignal eşdeğeri): target.email_to (e-posta) ve target.include_phone_numbers (E.164) verilen adreslere gönderir; aboneliği olmayan adres için anonim kullanıcı + abonelik oluşturulur ve hedefe include_subscription_ids olarak eklenir (ham adres mesaj hedefinde saklanmaz). Bu alanlar ilgili kanalı açıkça hedefler (SMS dahil). Geçersiz adres 400.

include_unsubscribed: true (yalnızca işlemsel e-posta; şifre sıfırlama, makbuz): abonelikten çıkmış e-posta adresleri de hedeflenir ve unsubscribe_link suppression kaydı yok sayılır; bounce, şikâyet, manuel ve STOP kayıtları her zaman uygulanır. Push ve SMS abonelikleri etkilenmez.

Yalnızca e-posta gönderilen mesajda contents zorunlu değildir; email_subject + email_mjml/email_body yeterlidir.

Kanal verilmezse (target.channels / kampanya audience.channels boş) kanallar içerikten çıkarılır: contents (veya content_available) varsa push kanalları, email_* alanlarından biri varsa e-posta. SMS yalnızca açıkça channels: ["sms"] ile hedeflenir. Böylece push kampanyası e-posta/SMS abonelerine push metniyle gitmez. Hedeflenen her kanalın içeriği oluşturma/gönderim anında doğrulanır (ör. SMS için contents, e-posta için konu + gövde); eksikse 422 HS_CONTENT_INVALID.

E-posta HTML gövdelerinde Liquid çıktıları HTML kaçışlıdır (kullanıcı etiketleriyle işaretleme enjekte edilemez); bilinçli olarak ham HTML gerekiyorsa {{ değer | raw }}. {{ unsubscribe_url }} değişkeni teslimata özel abonelikten çıkma adresine dönüşür; şablonda kullanılmazsa altbilgi otomatik eklenir (dil: alıcı dili, tr/en).

Önizleme: POST /internal/v1/apps/{app_id}/templates/render (channel: "email" → email.{subject, preheader, html, text}; channel: "sms" → sms.{body, encoding, segments, per_segment, non_gsm_characters, turkish_characters}).

3. E-posta izleme ve abonelikten çıkma

Her e-postaya teslimata özgü, MASTER_KEY'den türetilen anahtarla HMAC imzalı adresler eklenir (API_URL kökü):

AdresDavranış
GET /t/o/{dlv}?a=&s=1×1 GIF; ilk istekte opened (+ gerekiyorsa delivered). İmza geçersizse yalnızca GIF döner.
GET /t/c/{dlv}?a=&u=&s=İmza hedef URL'yi kapsar; doğrulanırsa clicked kaydı + 302. İmza tutmazsa 404 (açık yönlendirme yok).
GET /t/u/{dlv}?a=&s=Onay sayfası (tr/en, açık/koyu); GET abonelikten çıkarmaz (bağlantı tarayıcıları tetiklemesin).
POST /t/u/{dlv}?a=&s=RFC 8058 tek tık (List-Unsubscribe=One-Click) ve onay formu: abonelik unsubscribed, suppression unsubscribe_link.
GET /t/s/{code}SMS kısa linki (8 karakter base62, 90 gün): tıklama sayılır, 302.

Başlıklar: List-Unsubscribe: <https://…/t/u/…>, List-Unsubscribe-Post: List-Unsubscribe=One-Click, X-HS-Delivery-Id. Düz metin sürüm (text/plain) HTML'den üretilir.

4. SMS

  • SDK'lar e-posta/SMS aboneliğini POST /sdk/v1/apps/{app_id}/subscriptions (channel: "email"|"sms") ile ekler: login'li kullanıcıda user.external_id (+identity_hash), anonim kullanıcıda cihazın abonelik kimliği user.subscription_id gönderilir; yeni abonelik o kullanıcıya bağlanır.
  • Numaralar E.164 (+905321234567) tutulur; SDK/API kaydında boşluk, tire, parantez ve 00 öneki normalize edilir, geçersiz değer 400.
  • Segment sayımı: GSM-7 tek parça 160 / çok parça 153; UCS-2 70 / 67. ş ğ ı İ ç GSM-7 dışındadır ve mesajı UCS-2'ye düşürür (dashboard uyarır). Netgsm encoding: TR (Türkçe tek kaydırma tablosu) ile bu harfler 2 septet sayılır: tek parça 155, çok parça 149. Gönderilen parça sayısı teslimat olayında (sent.detail.segments) ve mesaj sayacında (counts.sms_segments) tutulur.
  • STOP/START: Twilio gelen mesaj webhook'u (Advanced Opt-Out OptOutType ya da anahtar sözcük), Netgsm gelen kutusu sorgusu veya genel webhook (type: "stop"|"start"). STOP → suppression stop + abonelik unsubscribed; START → stop kaydı silinir, abonelik yeniden subscribed. Anahtar sözcükler: STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, İPTAL/IPTAL, RET, DUR / START, YES, UNSTOP, BAŞLA/BASLA.
  • HELP ve otomatik yanıt (Faz 12): HELP, INFO, YARDIM, BILGI/BİLGİ → help; genel webhook type: "help". apps.settings.sms.autoReply açıkken helpReply / stopReply metni tek aboneliğe işlemsel SMS olarak gönderilir (STOP sonrası da iletilir). Ayrıntı: preferences.md §5.
  • Netgsm push webhook sunmaz: worker dakikada bir aktif Netgsm kanallarının teslim raporunu (/sms/rest/v2/report, son 72 saat) ve gelen SMS'lerini (/sms/rest/v2/inbox) sorgular. Rapor durumu 1 → delivered, 3 → geçersiz numara (abonelik invalid), 15 → kara liste (stop suppression), diğerleri failed. Yalnızca +90 numaralara gönderilir.

5. Gelen sağlayıcı webhook'ları

Adres dashboard'da Kanallar → E-posta/SMS → Webhook adresi ile gösterilir (gizli token içerir; görüntüleme denetim kaydına yazılır) ve Yenile ile rotasyon yapılır (eski adres hemen 404). Biçim: {API_URL}/hooks/{email|sms}/{provider}/{token}.

SağlayıcıKurulumDoğrulamaİşlenen olaylar
SESConfiguration set → SNS konusu → HTTPS aboneliği bu adreseSNS imzası (SigningCertURL yalnızca sns.<bölge>.amazonaws.com), konu (TopicArn) bölgesi kanalın SES bölgesiyle aynı olmalı, abonelik otomatik onaylanırBounce (Permanent → bounce, diğerleri soft), Complaint, Delivery, Open, Click, Reject
ResendWebhooks → endpoint = bu adres; signing secret'ı kanal ayarına webhook_secret olarak girinSvix imzası (5 dk tolerans)email.delivered/bounced/complained/opened/clicked/failed
TwilioGönderimde StatusCallback otomatik eklenir; numaranın A message comes in adresine de aynı URL'yi girinX-Twilio-Signature (auth token)delivered, undelivered/failed (+ErrorCode), gelen STOP/START
Genel (smtp, mock)Kendi ESP/MTA'nızdan JSON POSTURL token'ıaşağıdaki gövde

Genel gövde: { "type": "delivered|bounced|soft_bounced|complained|opened|clicked|failed|unsubscribed|stop|start|help", "delivery_id"?: "dlv_…", "provider_message_id"?: "…", "address"?: "+90…", "url"?, "error_code"?, "error_message"? }.

Etkiler: bounce → teslimat bounced, abonelik invalid, suppression bounce, counts.bounced, webhook delivery.failed (error_code: bounced) ve subscription.invalidated; complaint → suppression complaint, abonelik unsubscribed; senkron SMTP 550 de suppression bounce yazar. Aynı olayın tekrarı sayaçları ikinci kez artırmaz.

6. Suppression list

E-posta/SMS gönderilmeyecek adresler; ham değer saklanmaz (normalize + SHA-256 özet ve maskeli gösterim a***e@example.com). Audience aşamasında ve gönderim anında kontrol edilir; bastırılan teslimat suppressed + suppression_reason: suppression_list.

  • GET /v1/apps/{app_id}/suppressions?channel=&reason=&value= (users:read), POST { channel, value, reason?, expires_at?, note? } (users:write), DELETE /{id}.
  • Dashboard: Kitle → Suppression list (internal …/suppressions, okuma users.read, yazma users.write).
  • Nedenler: bounce, complaint, manual, unsubscribe_link, stop. "Verileri temizle" suppression list'i korur.

7. In-app mesajlar

Dashboard Mesajlar → In-App (internal …/in-app-messages; düzenleme campaigns.create, yayınlama campaigns.send). Ham HTML yoktur; mesaj bloklardan oluşur ve SDK'lar yerel bileşenlerle çizer.

{
  "name": "Sepet hatırlatma",
  "triggers": [{ "kind": "event", "name": "cart_viewed" }],
  "display": {
    "layout": "modal",
    "background_color": "#ffffff",
    "text_color": "#18181b",
    "dismissible": true,
    "blocks": [
      { "type": "image", "url": "https://cdn.example.com/sale.png" },
      { "type": "title", "text": { "tr": "Merhaba {{ user.tags.name }}", "en": "Hi {{ user.tags.name }}" } },
      { "type": "body", "text": { "tr": "Sepetin seni bekliyor", "en": "Your cart is waiting" } },
      { "type": "button", "id": "go", "text": { "tr": "Sepete git", "en": "Open cart" }, "action": { "type": "deeplink", "url": "myapp://cart" } }
    ]
  },
  "audience": { "included_segments": ["seg_…"], "excluded_segments": [] },
  "frequency": { "max_impressions": 3, "min_interval_seconds": 86400, "stop_after_interaction": true },
  "priority": 10,
  "start_at": null,
  "end_at": null
}
  • Düzenler: modal, banner_top, banner_bottom, fullscreen; banner'da auto_dismiss_seconds.
  • Tetikler (hepsi sağlanmalı): session_start, event (name), screen (name), trigger (key, op: eq|neq|exists|gt|lt, value). trigger koşulları SDK'daki InAppMessages.addTrigger(key, value) değerleriyle karşılaştırılır.
  • Aksiyonlar: dismiss, url (http/https, tarayıcı), deeplink (uygulama şeması), tags (kullanıcıya etiket yazar).
  • Kitle: segment dahil/hariç; push izni olmayan cihazlar da dahildir (segment koşulu abonelik durumuna bakmadan değerlendirilir).
  • Sıklık: kullanıcı başına (kimliksiz cihazda cihaz başına) toplam gösterim, iki gösterim arası süre, etkileşim sonrası durdurma. Varsayılan max_impressions: 1.
  • İstatistik: gösterim, tıklama, kapatma, benzersiz kullanıcı, CTR.

SDK sözleşmesi:

  • GET /sdk/v1/apps/{app_id}/in-app?subscription_id= → { language, data: [{ id, priority, triggers, display{layout, background_color, text_color, dismissible, auto_dismiss_seconds, blocks[]}, remaining_impressions, min_interval_seconds, stop_after_interaction, last_impression_at, end_at }] }. Bloklardaki metinler kullanıcının dilinde çözülmüş ve kişiselleştirilmiş düz dizedir (title.text, body.text, button.text, image.alt).
  • POST /sdk/v1/apps/{app_id}/in-app/{message_id}/events { subscription_id, type: "impression"|"click"|"dismiss", button_id? } (click için button_id zorunlu).
  • SDK davranışı: oturum başında ve en fazla 5 dakikada bir listeyi yeniler; aynı anda tek mesaj gösterir (öncelik yüksek olan); gösterimde impression, düğmede click, kapatmada dismiss bildirir; remaining_impressions ve min_interval_seconds yerelde de uygulanır.

8. Bildirim kutusu (inbox)

add_to_inbox: true mesajlarında hedeflenen (bastırılmamış) her kullanıcı için bir öğe oluşur (kullanıcının dilinde başlık/gövde, app_url ya da url, big_picture, data). Aynı mesaj aynı kullanıcıya bir kez eklenir; inbox_ttl dolunca öğe görünmez ve günlük bakımda silinir.

SDK uçları (abonelik kullanıcıya ait olmalı; app kimlik doğrulaması istiyorsa identity_hash):

  • GET /sdk/v1/apps/{app_id}/users/{user_id}/inbox?subscription_id=&identity_hash=&limit=&cursor= → { data: [{ id, message_id, title, body, url, image_url, data, read, read_at, archived_at, expires_at, created_at }], unread_count, next_cursor, has_more } (yeniden eskiye).
  • POST …/inbox/{item_id}/read { subscription_id, identity_hash? } → öğe.
  • POST …/inbox/read-all { subscription_id, identity_hash? } → { updated, unread_count }.
  • DELETE …/inbox/{item_id}?subscription_id=&identity_hash= → arşivler.

Public API (API anahtarı; okuma users:read, değiştirme users:write; kullanıcı alias ile seçilir):

  • GET /v1/apps/{app_id}/users/by/{label}/{value}/inbox?include_archived=&limit=&cursor=
  • POST …/inbox/{item_id}/read, POST …/inbox/read-all, DELETE …/inbox/{item_id}

In-app mesajlar da public API ile yönetilebilir: GET|POST /v1/apps/{app_id}/in-app-messages, GET|PATCH|DELETE …/{id}, POST …/{id}/status (okuma analytics:read, yazma admin).

Dashboard: kullanıcı detayında Bildirim kutusu (GET /internal/v1/apps/{app_id}/users/{user_id}/inbox?include_archived=).

9. Kişisel veri

Kullanıcı silindiğinde inbox öğeleri (FK) ve in-app gösterim kayıtları silinir. Suppression list ham adres tutmaz. "Verileri temizle" in-app gösterimlerini, inbox'ı ve kısa linkleri siler; in-app mesaj tanımlarını ve suppression list'i korur.

10. Metrikler

/metrics: messaging_feedback_total{source,type} (kaynak: hook:ses, hook:resend, hook:twilio, hook:smtp, hook:mock, tracking, short_link, unsubscribe_link, netgsm; tür: delivered, opened, clicked, bounced, soft_bounced, complained, failed, unsubscribed) ve suppression_entries_total{channel,reason}.

11. Yerel geliştirme

  • Mailpit (smtp://localhost:1025, arayüz http://localhost:8025): e-posta kanalını smtp, host localhost (Mailpit yalnızca ::1 üzerinde dinleyebilir), port 1025, secure: false, kullanıcı adı boş olarak kaydedip Doğrula.
  • Mock e-posta: yerel kısmı invalid/bounce → kalıcı ret, transient, rate, auth, fail, open (5 sn sonra açılma) ile başlayan adresler.
  • Mock SMS: numaranın son dört hanesi 0001 geçersiz, 0002 kalıcı, 0003 geçici, 0004 hız sınırı, 0005 kimlik hatası; diğerleri başarılı.
  • Genel webhook ile bounce/STOP denemesi: curl -X POST "$(webhook adresi)" -H 'content-type: application/json' -d '{"type":"stop","address":"+905320000111"}'.

Bu sayfada