npm install signalbird · composer require pariette/signalbird · pip install signalbirdGönderim (Messaging) SDK - Node.js & PHP / Laravel
Signalbird Gönderim istemcisi. Sunucunuzdan e-posta, SMS ve push gönderir; kişileri ve listeleri yönetir; kampanya açar; mesaj durumlarını okur ve webhook imzalarını doğrular.
Telsiz istemcisinden (SignalbirdClient / Signalbird::log()) ayrıdır:
farklı anahtar, farklı kapı, farklı kota. Aynı pakettedir, ayrı sınıftır.
takım anahtarı → sb_… (Konsol → SDK anahtarları)
kapsam → email:send, contacts:write… (anahtar açılırken seçilir)
sınıf → transactional | commercial (her gönderimde ZORUNLU)Anahtar ve kapsamlar
Panelde Konsol → SDK anahtarları → Yeni anahtar ile bir takım anahtarı
(sb_…) açın. Anahtar size bir kez gösterilir; gizlidir ve yalnız
sunucuda kullanılır - tarayıcıya ya da mobil uygulamaya gömülmez.
Anahtar açılırken kapsam seçilir. Bir metodun çalışması için ilgili kapsamın
anahtarda olması gerekir; yoksa API_KEY_SCOPE döner.
| Kapsam | Açtığı işlemler |
|---|---|
email:send · sms:send · push:send |
sendEmail, sendSms, previewSms, sendPush |
contacts:read · contacts:write |
kişi ve liste okuma / yazma, bulkContacts |
campaigns:read · campaigns:write |
kampanya listeleme, açma, iptal |
email:read · sms:read · push:read |
mesaj günlüğü (listMessages, getMessage) |
Yanlış türde anahtar (açık anahtar sb_public_live_…) verilirse istemci
kurulurken WRONG_KEY_TYPE fırlatır - haftalar sonra bulunacak bir 401
yerine ilk saniyede.
Kurulum
npm install signalbird # Node 18+, bağımlılık yok
composer require pariette/signalbird # PHP 8.1+, ext-curl, Guzzle gerekmezHızlı başlangıç
Node.js / TypeScript
import { SignalbirdMessaging } from 'signalbird'
const sb = new SignalbirdMessaging({ domainKey: process.env.SIGNALBIRD_DOMAIN_KEY! })
const res = await sb.sendEmail({
to: 'ahmet@example.com',
class: 'transactional',
subject: 'Siparişiniz alındı',
body: '<p>Sipariş #1042 hazırlanıyor.</p>',
})
if (res.ok) console.log(res.data.id, res.data.status) // 'msg_…', 'queued'
else console.warn(res.code, res.message) // 'NO_SENDING_DOMAIN', …Yapılandırma: { domainKey, baseUrl?, timeout?, throwOnError?, debug? } -
varsayılanlar https://signalbird.io/api, 15000 ms, false, false.
PHP
use Signalbird\Sdk\Messaging\MessagingClient;
$sb = new MessagingClient(getenv('SIGNALBIRD_DOMAIN_KEY'));
$res = $sb->sendSms([
'to' => '+905321234567',
'class' => 'transactional',
'body' => 'Doğrulama kodunuz: 482913',
]);
if ($res['ok']) { $id = $res['data']['id']; }
else { $code = $res['code']; }Kurucu: new MessagingClient(string $domainKey, ?string $baseUrl = null, int $timeout = 15, bool $throwOnError = false).
Laravel
# .env
SIGNALBIRD_DOMAIN_KEY=sb_xxxxxxxxxxxxxxxxxxxxxxxx
# SIGNALBIRD_MESSAGING_URL=https://signalbird.io/api # opsiyoneluse Signalbird\Sdk\Facades\Signalbird;
$res = Signalbird::messaging()->sendPush([
'to' => 'external:user-1042',
'class' => 'commercial',
'subject' => 'Hafta sonu indirimi',
'body' => 'Sepetinizdeki ürünlerde %20',
]);Signalbird::messaging() config('signalbird.messaging_key') ile kurulur ve
tekildir. Telsiz metotları (Signalbird::info() vb.) etkilenmez.
Metotlar
Node ve PHP'de metot adları birebir aynıdır; Node nesne alır, PHP dizi. Alan adları API ile aynıdır (snake_case) - SDK yeniden adlandırma yapmaz.
Şablon seçimi iki biçimdedir ve biri yeterlidir: template (panelde yazan
AD, büyük/küçük harfe duyarsız) ya da template_id (sayı).
Şablon verdiğinizde subject ve body isteğe bağlıdır - konu şablondan
gelir, ama siz konu yazdıysanız sizinki kazanır. Bulunamayan şablon 422
TEMPLATE_NOT_FOUND döner; gövdesiz posta gönderilmez.
| Alan | Metot | HTTP | Not |
|---|---|---|---|
| Gönderim | sendEmail({ to, class, subject?, body?, template?, template_id?, vars?, sending_domain_id?, sending_address_id?, contact_id?, from_name?, reply_to?, attachments?, module_key? }) |
POST /v1/email/send |
202 → { id, status, units } |
| Gönderim | sendSms({ to, class, body, brand_id?, contact_id? }) |
POST /v1/sms/send |
E.164 numara |
| Gönderim | previewSms(body) |
POST /v1/sms/preview |
Parça/karakter hesabı, kota harcamaz |
| Gönderim | sendPush({ to, class, subject, body, vars?, contact_id? }) |
POST /v1/push/send |
to: token, contact:<id> ya da external:<id> |
| Kişiler | listContacts({ q?, list_id?, page?, per_page? }) |
GET /v1/contacts |
Sayfalı |
| Kişiler | createContact({ email?, phone?, first_name?, last_name?, attributes?, list_ids?, consent_source?, consent_text? }) |
POST /v1/contacts |
|
| Kişiler | updateContact(id, partial) |
PATCH /v1/contacts/{id} |
|
| Kişiler | deleteContact(id) |
DELETE /v1/contacts/{id} |
|
| Kişiler | bulkContacts({ contacts, list_id?, consent_source?, consent_text? }) |
POST /v1/contacts/bulk |
1000'lik parçalar, sıralı |
| Listeler | listContactLists() |
GET /v1/contact-lists |
|
| Listeler | createContactList({ name, description? }) |
POST /v1/contact-lists |
|
| Listeler | deleteContactList(id) |
DELETE /v1/contact-lists/{id} |
|
| Kampanyalar | listCampaigns({ status?, channel?, page?, per_page? }) |
GET /v1/campaigns |
|
| Kampanyalar | createCampaign({ name, channel, domain_id, list_id | segment_id, body, subject?, sending_domain_id?, brand_id?, scheduled_at?, metadata?, external_ref? }) |
POST /v1/campaigns |
202 → { batch, class, summary } |
| Kampanyalar | getCampaign(id) |
GET /v1/campaigns/{id} |
{ batch, status_breakdown, jobs } |
| Kampanyalar | cancelCampaign(id) |
POST /v1/campaigns/{id}/cancel |
Bitmişse ALREADY_FINISHED |
| Kampanyalar | listCampaignMessages(id, { page?, per_page?, status? }) |
GET /v1/campaigns/{id}/messages |
Sayfalı |
| Kampanyalar | iterateCampaignMessages(id, query?) |
(çoklu GET) | Node: for await; PHP: Generator. Sayfa alınamazsa fırlatır |
| Mesajlar | listMessages({ status?, channel?, batch_id?, page?, per_page? }) |
GET /v1/messages |
Alıcı maskeli döner |
| Mesajlar | getMessage(id) |
GET /v1/messages/{id} |
classzorunludur ve varsayılanı yoktur.transactionalmıcommercialmi olduğuna SDK karar vermez - bu hukuki bir kapıdır. Ticari iletilerde onay (NO_CONSENT) ve bastırma listesi (SUPPRESSED) uygulanır.
Sonuç biçimi ve hata yönetimi
Her metot aynı biçimde döner:
// Node
type SbResult<T> =
| { ok: true; status: number; data: T }
| { ok: false; status: number; code: string; message: string; data?: unknown }// PHP
['ok' => bool, 'status' => int, 'data' => ?array, 'code' => ?string, 'message' => ?string]Sessiz hata varsayılandır. Ağ koparsa, kota dolarsa ya da alıcı bastırma
listesindeyse çağrı ok: false + code döner ve uygulamanız çalışmaya devam
eder. Sipariş akışını bir e-posta hatası çökertmemelidir.
Geliştirmede ya da hatanın yukarı taşınmasını istediğiniz yerlerde
throwOnError: true:
const sb = new SignalbirdMessaging({ domainKey, throwOnError: true })
try {
await sb.sendEmail({ … })
} catch (e) {
if (e instanceof SignalbirdError) console.error(e.code, e.status, e.data)
}$sb = new MessagingClient($key, throwOnError: true);
try {
$sb->sendEmail([...]);
} catch (\Signalbird\Sdk\SignalbirdException $e) {
$e->getErrorCode(); // 'LIMIT_REACHED'
$e->getStatus(); // 429
$e->getBody(); // API'nin ham yanıtı
}Yeniden deneme yoktur. Aynı iletiyi iki kez göndermek, hiç göndermemekten
pahalıdır; NETWORK_ERROR / TIMEOUT sonrası tekrar kararı çağıranındır.
Toplu kişi yükleme
const list = await sb.createContactList({ name: 'Bülten - Ağustos' })
const res = await sb.bulkContacts({
list_id: list.data.id,
consent_source: 'web-form',
consent_text: 'Kampanya e-postası almayı kabul ediyorum.',
contacts: rows.map(r => ({
email: r.email,
first_name: r.name,
attributes: { plan: r.plan },
})),
})
// { imported: 4120, updated: 310, skipped: [{ email: 'x', reason: 'invalid' }, …] }bulkContacts diziyi 1000'lik parçalara böler ve sırayla gönderir (paralel
değil: aynı e-posta iki parçada da varsa yarış olmasın). Sayımlar tek yanıtta
birleşir. Bir parça başarısız olursa orada durulur; o ana kadar biriken sayım
data içinde döner - kaç kişinin işlendiğini görürsünüz.
Kampanya akışı
// 1. liste + kişiler
const list = await sb.createContactList({ name: 'Yeni özellik duyurusu' })
await sb.bulkContacts({ list_id: list.data.id, contacts })
// 2. kampanyayı aç (hemen ya da zamanlanmış)
const created = await sb.createCampaign({
name: 'Ağustos duyurusu',
channel: 'email',
domain_id: 7, // TXT ile doğrulanmış alan adınız (zorunlu)
list_id: list.data.id,
subject: 'Yeni: takvim entegrasyonu',
body: '<p>Merhaba {{first_name}}, …</p>',
scheduled_at: '2026-08-21T09:00:00+03:00',
external_ref: 'crm-cmp-77',
})
// created.data.summary → { total, queued, skipped, stopped_reason }
// 3. durumu izle
const detail = await sb.getCampaign(created.data.batch.id)
// detail.data.status_breakdown → { queued: 120, sent: 3900, bounced: 12, … }
// 4. mesajları gez
for await (const m of sb.iterateCampaignMessages(created.data.batch.id, { status: 'bounced' })) {
console.log(m.recipient, m.last_code)
}listCampaignMessages tek sayfa döner; iterateCampaignMessages sayfaları
kendisi gezer ve bir sayfa alınamazsa fırlatır - sessiz yarım liste "hepsi
bu" sanılır, o daha tehlikelidir. cancelCampaign yalnız bitmemiş partilerde
çalışır.
Webhook doğrulama
Panelde Konsol → Webhooks ile bir uç nokta tanımlayın. Her teslimat şu başlıklarla gelir:
| Başlık | İçerik |
|---|---|
X-Signalbird-Event |
message.queued · sent · delivered · bounced · failed · opened · clicked · unsubscribed · complained · campaign.finished · campaign.canceled |
X-Signalbird-Delivery |
Teslimat kimliği (tekrarları ayıklamak için) |
X-Signalbird-Signature |
sha256=<hex hmac-sha256(ham_gövde, secret)> |
X-Signalbird-Timestamp |
Unix zaman damgası |
Gövde: { id, type, occurred_at, team_id, data: { message_id, batch_id, channel, contact_id, external_ref, batch_external_ref, status, code, detail, recipient } }.
2xx dışı yanıtta 3 kez yeniden denenir (1 dk / 5 dk / 30 dk).
İmza ham gövde üzerinden hesaplanır. JSON'u ayrıştırıp yeniden serileştirmek anahtar sırasını değiştirir ve imzayı bozar.
Express
import express from 'express'
import { verifyWebhook } from 'signalbird'
app.post('/webhooks/signalbird', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyWebhook(req.body, req.header('X-Signalbird-Signature'), process.env.SIGNALBIRD_WEBHOOK_SECRET!)
if (!ok) return res.status(401).end()
const event = JSON.parse(req.body.toString('utf8'))
if (event.type === 'message.bounced') { /* … */ }
res.status(204).end()
})Laravel
use Illuminate\Http\Request;
use Signalbird\Sdk\Messaging\Webhook;
Route::post('/webhooks/signalbird', function (Request $request) {
$ok = Webhook::verify(
$request->getContent(), // ham gövde
$request->header('X-Signalbird-Signature'),
config('services.signalbird.webhook_secret'),
);
abort_unless($ok, 401);
$event = $request->json()->all();
// $event['type'], $event['data']['message_id'] …
return response()->noContent();
})->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);Hata kodları
| Kod | Anlamı |
|---|---|
API_KEY_MISSING · API_KEY_INVALID |
Anahtar yok, yanlış ya da iptal edilmiş |
API_KEY_SCOPE |
Anahtarda bu işlem için kapsam yok |
API_KEY_IP_BLOCKED |
İstek, anahtarın IP izin listesi dışından geldi |
API_KEY_NO_TEAM |
Anahtarın bağlı olduğu takım yok / pasif |
MODULE_DISABLED |
Paketinizde bu kanal (email/sms/push) yok |
LIMIT_REACHED |
Aylık kota doldu |
OVERAGE_CEILING_REACHED |
Aşım tavanına ulaşıldı - panelden yükseltilebilir |
SUPPRESSED |
Alıcı bastırma listesinde (bounce / şikâyet / abonelikten çıkma) |
NO_CONSENT |
Ticari ileti için kişinin onayı yok |
NO_SENDING_DOMAIN |
Doğrulanmış gönderim alanı yok |
INVALID_PHONE |
Numara E.164 biçiminde değil |
LIST_NOT_FOUND |
list_id bu takıma ait değil |
NO_RECIPIENTS |
Listede gönderilebilir kişi yok |
ALREADY_FINISHED |
Kampanya bitmiş, iptal edilemez |
VALIDATION_ERROR |
Alan doğrulaması (HTTP 422); ayrıntı data.errors |
NETWORK_ERROR · TIMEOUT |
İstek gitmedi ya da zaman aşımı - tekrar kararı sizin |