API Dokümantasyonu

ERP'nizden, e-ticaret altyapınızdan ya da çağrı merkezinizden WhatsApp mesajı gönderin, müşteri defterinizi besleyin, her olayı webhook ile anında yakalayın.

Taban adres
https://app.flowoy.com
Sürüm
v1
Kimlik
X-API-Key
Sınır
120 istek/dk
Plan
Profesyonel

Hızlı başlangıç

Kargo bildirimi kuran bir ekibin izlediği yol — beş çağrı, sırayla.

# 1 — anahtar doğru mu, hangi hesaba bağlı?
curl https://app.flowoy.com/api/v1/me -H "X-API-Key: $KEY"

# 2 — hangi şablon kaç değişken istiyor?
curl https://app.flowoy.com/api/v1/templates -H "X-API-Key: $KEY"

# 3 — müşteriyi deftere yaz (izniyle birlikte)
curl -X POST https://app.flowoy.com/api/v1/customers \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"phone":"05551112233","firstName":"Ayşe","marketingConsent":true}'

# 4 — kargoya verilince şablonu gönder
curl -X POST https://app.flowoy.com/api/v1/messages/template \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"phone":"05551112233","template":"kargo_takip_butonlu",
       "variables":{"1":"Ayşe","2":"SP-1042","3":"Aras Kargo",
                    "4":"7788991122","button_0_1":"xCpXi4v"}}'

# 5 — sonucu webhook ile bekleyin; webhook yoksa durumu sorun
curl https://app.flowoy.com/api/v1/messages/96a67fd4-… -H "X-API-Key: $KEY"

Dördüncü adımın döndürdüğü messageId değerini kendi sisteminizde siparişin yanına saklayın. Bir sorun olduğunda destekle konuşurken tek referans odur.

Kimlik doğrulama

Her istek X-API-Key başlığı ister. Anahtarsız istek 401 döner — API ile hesabınız olmadan hiçbir işlem yapılamaz.

curl https://app.flowoy.com/api/v1/me \
  -H "X-API-Key: ptf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Anahtar nereden alınır

Panel → Ayarlar → API Anahtarları → Yeni Anahtar. Anahtar yalnız bir kez gösterilir; kaybederseniz yenisini üretip eskisini silin.

Anahtarı sorgu dizgisinde göndermeyin. ?apiKey=… biçimi kabul edilmez. URL'ler sunucu erişim kayıtlarına, referrer başlıklarına ve tarayıcı geçmişine düşer; anahtar oralardan sızar. Aynı sebeple anahtarı tarayıcıda çalışan koda gömmeyin — çağrıyı kendi sunucunuzdan yapın.

  • Anahtar hesabınıza bağlıdır; başka hesabın verisine erişemez.
  • API erişimi Profesyonel planında açıktır. Plan düşerse mevcut anahtarlar da 403 plan_required döner.
  • Anahtar biçimi ptf_ + 32 karakter. İlk 12 karakter arama ön eki olarak saklanır, gerisi yalnız hash hâlinde durur — bizde bile düz metin karşılığı yoktur.

Hız sınırı

Anahtar başına dakikada 120 istek. Her yanıt kalan hakkı X-RateLimit-Remaining başlığında taşır. Sınır aşılınca 429 ve Retry-After döner — o kadar saniye bekleyip devam edin.

Bunun dışında gönderim, WhatsApp numaranızın kendi saatlik ve günlük kotasına da tabidir. Toplu iş yaparken iki sınırı da gözetin; 25'lik partiler için /messages/bulk ucunu kullanın.

Yanıt ve hatalar

Başarılı her yanıt ok: true, her hata ok: false ve makine tarafından okunabilir bir error kodu taşır.

HTTPerrorAnlamı
400bad_requestİstek eksik ya da bozuk
400variablesŞablon değişkenleri uyuşmuyor — ayrıntı missing / unexpected alanlarında
400templateŞablon Meta'da onaylı değil
401unauthorizedAnahtar yok ya da geçersiz
403plan_requiredPlanınızda API erişimi yok
403not_sendableGönderim izin kapısından geçmedi — reason sebebi yazar
404not_foundKayıt bulunamadı
413too_manyToplu gönderimde alıcı sayısı sınırı aşıldı
429rate_limitedHız sınırı
500internalBizdeki hata — tekrar denenebilir

403 not_sendable bir hata değil, kuraldır. Müşteri pazarlama iznini kapatmış, kara listeye düşmüş ya da sıklık sınırı dolmuş olabilir. Tekrar denemeyin — gönderim kapıları bölümü hangi kapıya takıldığınızı anlatır.

Değişken hatası neye benzer

Eksik parametreyi Meta'nın 132000 ailesinden değil, bizden ve adıyla öğrenirsiniz:

{
  "ok": false,
  "error": "variables",
  "missing": ["button_0_1"],
  "unexpected": [],
  "expected": {
    "bodyVariableCount": 4,
    "dynamicButtons": [{ "anahtar": "button_0_1", "metin": "Kargomu takip et" }]
  }
}

Uç noktalar

GET/api/v1/me

Anahtarın hangi hesaba ait olduğunu doğrular. Entegrasyonu kurarken ilk burayı çağırın.

{ "ok": true, "data": {
  "company": "Örnek Market",
  "plan": "proexpert",
  "phone": "+90 5xx xxx xx xx",
  "rateLimitPerMinute": 120
} }
GET/api/v1/templates

Yalnız Meta'da onaylı şablonlarınız. Gönderim yapmadan önce buradan hangi şablonun kaç değişken ve hangi buton anahtarını istediğini öğrenin. Tek şablon için /api/v1/templates/{ad}.

{ "ok": true, "total": 1, "data": [{
  "name": "kargo_takip_butonlu",
  "category": "UTILITY",
  "language": "tr",
  "body": "Merhaba {{1}}, {{2}} numaralı siparişiniz {{3}} ile kargoya verildi. Takip numaranız {{4}}.",
  "headerType": null,
  "bodyVariableCount": 4,
  "dynamicButtons": [{ "anahtar": "button_0_1", "metin": "Kargomu takip et" }]
}] }

bodyVariableCount kadar "1", "2"… anahtarı ve her dynamicButtons[].anahtar değerini göndermeniz gerekir.

GET/api/v1/customers

Müşteri listeniz. limit en çok 1000, varsayılan 100; offset ile sayfalayın. Tek kişi için /api/v1/customers/{telefon}.

POST/api/v1/customers

Telefona göre ekler ya da günceller. Aynı numarayı tekrar göndermek mükerrer kayıt açmaz — dış sistemler aynı müşteriyi sürekli ittiği için bu davranış kasıtlıdır. Yeni kayıt 201, güncelleme 200 döner; created alanı hangisi olduğunu söyler.

AlanZorunluNot
phoneevet05xx…, +905xx…, 905xx… kabul edilir
firstNamehayırGöndermediğiniz alana dokunulmaz
lastNamehayır
emailhayır
marketingConsenthayırYalnız açıkça true/false gönderirseniz değişir. Göndermezseniz mevcut izin korunur — izin kaydı yasal belgedir, kazara silinmez
POST/api/v1/customers/{telefon}/opt-out

Pazarlama çıkışı. Kendi iptal akışınızı (sitenizdeki "listeden çık" bağlantısı, çağrı merkezi) buraya bağlayın — aksi hâlde izin iki ayrı yerde tutulur ve biri yanlış olur. 6563 sayılı kanun gereği izin geri alındığında iznin kendisi de kapanır, yalnız bayrak değil. Aynı anda customer.opted_out webhook'u yayınlanır.

POST/api/v1/messages/template

Onaylı şablon gönderir. 202 döner: mesaj kuyruğa alındı, henüz teslim edilmedi.

curl -X POST https://app.flowoy.com/api/v1/messages/template \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "phone": "05551112233",
    "template": "kargo_takip_butonlu",
    "createIfMissing": true,
    "variables": { "1":"Ayşe", "2":"SP-1042", "3":"Aras Kargo",
                   "4":"7788991122", "button_0_1":"xCpXi4v" }
  }'

# → 202
{ "ok": true, "messageId": "96a67fd4-…", "status": "queued" }
AlanZorunluNot
phoneevetAlıcı numarası
templateevetŞablon adı — uuid değil
variablesşablona göreGövde ve buton anahtarları
createIfMissinghayırVarsayılan false → numara defterde yoksa 404. Yazım hatasıyla sahte müşteri açılmasın diye böyle
firstName, lastNamehayırYalnız createIfMissing ile yeni kayıt açılırken kullanılır
POST/api/v1/messages/template · doğrulama kodu

SMS yerine WhatsApp'tan tek kullanımlık kod. Aynı uç, farklı kategoride bir şablon: AUTHENTICATION. Bu kategoride gövde metnini Meta kendisi yazar ve dile göre çevirir — siz yalnız kodu gönderirsiniz.

curl -X POST https://app.flowoy.com/api/v1/messages/template \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "phone": "05551112233",
    "template": "dogrulama_kodu",
    "createIfMissing": true,
    "variables": { "kod": "482913" }
  }'

# → 202
{ "ok": true, "messageId": "wamid.HBgMOTA1…", "status": "queued" }

Kullanıcıya düşen mesaj şöyle görünür — metin Meta'nındır, değiştirilemez:

Doğrulama kodunuz: 482913. Güvenliğiniz için bu kodu paylaşmayın.
Süresi 10 dakika sonra doluyor.
[ Kodu kopyala ]
AlanZorunluNot
variables.kodevetÜrettiğiniz kod. code, otp veya 1 adı da kabul edilir — kodu iki kez yazmanız gerekmez, buton parametresini biz dolduruyoruz
templateevetHesabınızda onaylı doğrulama şablonunun adı

Bilinmesi gerekenler

  • İzin aranmaz. Doğrulama mesajı hizmet mesajıdır; pazarlama onayı, opt-out ve sıklık sınırı uygulanmaz. Kayıtlı olmayan kullanıcıya da gider.
  • 24 saat kuralı etkilemez. Şablon mesajı pencereden bağımsız gider.
  • Kodu sunucunuzda üretin ve hash'leyerek saklayın. Şablonun alt bilgisi 10 dakika diyor; sunucudaki geçerlilik süresi de 10 dakika olmalı, yoksa kullanıcıya yanlış bilgi vermiş olursunuz.
  • API anahtarını mobil uygulamaya gömmeyin. Anahtar tüm hesabın yetkisini taşır; APK veya IPA içinden çıkarılabilir. İstek kendi sunucunuzdan gitmeli.
  • Kopyala butonu otomatik doldurma değildir. Kullanıcı basar, kod panoya kopyalanır. Uygulamanın kodu kendiliğinden okuması için Android'e özel ZERO_TAP buton tipi ve uygulama tarafında broadcast receiver gerekir.

Doğrulama şablonu normal şablonlardan farklı oluşturulur (gövde metni kabul edilmez; güvenlik uyarısı, kod geçerlilik süresi ve OTP butonu verilir). Panelden oluşturabilir ya da bizden isteyebilirsiniz.

POST/api/v1/messages/bulk

Tek şablon, çok alıcı — istek başına en çok 25 kişi. Her alıcı kendi kapılarından ayrı geçer: biri izinsizse diğerleri gider. Yanıt alıcı başına sonuç taşır, çünkü toplu işte "kısmen başarılı" normal durumdur.

curl -X POST https://app.flowoy.com/api/v1/messages/bulk \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "template": "kargo_takip_butonlu",
    "recipients": [
      { "phone":"05551112233", "variables":{"1":"Ayşe","2":"SP-1042","3":"Aras","4":"778899","button_0_1":"xCpXi4v"} },
      { "phone":"05449998877", "variables":{"1":"Mert","2":"SP-1043","3":"Aras","4":"778900","button_0_1":"Kd82Lp1"} }
    ]
  }'

# → 202 — kısmi başarı normaldir
{ "ok": true, "queued": 1, "failed": 1, "results": [
  { "phone": "05551112233", "ok": true, "messageId": "96a67fd4-…" },
  { "phone": "05449998877", "ok": false,
    "error": "not_sendable", "reason": "Müşteri pazarlama iznini kapattı" }
] }

Tüm alıcılar aynı değişkenleri alacaksa variables alanını gövdenin köküne bir kez yazabilirsiniz; alıcıdaki değer kökü ezer.

POST/api/v1/messages/text

Serbest metin. Yalnız müşteri son 24 saat içinde size yazdıysa gönderilebilir — bu WhatsApp'ın kuralı. Pencere kapalıysa 131047 alırsınız ve şablona düşmeniz gerekir. Bu uç müşteri açmaz; numara defterde yoksa 404.

curl -X POST https://app.flowoy.com/api/v1/messages/text \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"phone":"05551112233","text":"Siparişiniz hazır, bugün çıkıyor."}'
GET/api/v1/messages

Gönderim geçmişi — "son X saatte ne oldu" diye sormanın yolu. Mutabakat için her kimliği tek tek yoklamaktan iyidir. Filtreler: status, direction, phone, since (ISO 8601), limit (en çok 200), offset.

curl "https://app.flowoy.com/api/v1/messages?status=failed&since=2026-10-30T00:00:00Z" \
  -H "X-API-Key: $KEY"
GET/api/v1/messages/{id}

Tek gönderimin sonucu.

statusAnlamı
pendingKuyrukta, henüz Meta'ya gitmedi
sentMeta kabul etti
deliveredTelefona ulaştı
readOkundu
failedDüştü — sebep error alanında

Webhook aboneliği

Durumu yoklamak yerine biz size haber verelim. Her olay HTTPS adresinize imzalı olarak POST edilir.

POST/api/v1/webhooks
curl -X POST https://app.flowoy.com/api/v1/webhooks \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://sizin-siteniz.com/flowoy/webhook",
       "events":["message.delivered","message.failed","message.received"]}'

# → 201 — secret YALNIZ burada gösterilir, saklayın
{ "ok": true, "data": {
  "id": "8b3c8f5d-…",
  "url": "https://sizin-siteniz.com/flowoy/webhook",
  "events": ["message.delivered", "message.failed", "message.received"],
  "secret": "whsec_4Kc…"
} }

events boş bırakılırsa tüm olaylara abone olursunuz. Adres https olmalı ve genel bir alan adına işaret etmelidir; yerel ve iç ağ adresleri (localhost, 10.x, 169.254.x) reddedilir.

GET/api/v1/webhooks

Aboneliklerinizi, üst üste kaçıncı başarısızlıkta olduklarını ve son hatayı listeler. secret burada dönmez.

DELETE/api/v1/webhooks/{id}

Aboneliği siler.

Olaylar

OlayNe zamanveri içinde
message.sentMeta mesajı kabul ettimessageId, to, status, error, errorCode, at
message.deliveredTelefona ulaştı
message.readOkundu
message.failedDüştü
message.receivedMüşteri size yazdımessageId, from, type, text, at
customer.opted_outPazarlama izni kapandıphone, source, at
// Size gelen gövde
{
  "olayId": "c1f0a0f2-…",
  "olay": "message.delivered",
  "zaman": "2026-10-31T09:14:22.418Z",
  "veri": {
    "messageId": "96a67fd4-…",
    "to": "905551112233",
    "status": "delivered",
    "error": null,
    "errorCode": null,
    "at": "2026-10-31T09:14:21.000Z"
  }
}

Teslim garantisi en az bir kezdir. Geçici bir hatada aynı olay tekrar gönderilebilir — olayId ile tekilleştirin. Yanıt olarak 2xx döndürün; 4xx dönerseniz olay kalıcı reddedilmiş sayılır ve tekrar denenmez.

Üst üste 20 başarısız teslimden sonra abonelik otomatik kapatılır — ölü bir adrese sonsuza kadar vurmayız. Kapanan aboneliği GET /api/v1/webhooks ile görür, yenisini oluşturursunuz.

İmza doğrulama

Her iletim üç başlık taşır. Gövdeyi işlemeden önce imzayı doğrulayın; aksi hâlde adresinize herkes sahte olay gönderebilir.

Başlıkİçerik
X-Flowoy-EventOlay adı — gövdeyi okumadan yönlendirmek için
X-Flowoy-TimestampUnix saniye. 5 dakikadan eskiyse reddedin
X-Flowoy-Signaturesha256= + HMAC-SHA256(secret, "timestamp.ham_gövde")

Zaman damgası imzaya dahildir: aksi hâlde eski bir gövde aynı imzayla sonsuza kadar tekrar oynatılabilirdi.

PHP

$govde = file_get_contents('php://input');
$zaman = $_SERVER['HTTP_X_FLOWOY_TIMESTAMP'] ?? '';
$imza  = $_SERVER['HTTP_X_FLOWOY_SIGNATURE'] ?? '';

// 1) Tazelik — tekrar oynatmayı (replay) engeller
if (abs(time() - (int)$zaman) > 300) { http_response_code(400); exit; }

// 2) İmza — sabit zamanlı karşılaştırma
$beklenen = 'sha256=' . hash_hmac('sha256', $zaman . '.' . $govde, FLOWOY_SECRET);
if (!hash_equals($beklenen, $imza)) { http_response_code(401); exit; }

$olay = json_decode($govde, true);
// … olayId ile tekilleştirin, işinizi yapın
http_response_code(200);

Node.js

const crypto = require('crypto');

// DİKKAT: ham gövde gerekir — express.json() sonrası hesaplama tutmaz.
app.post('/flowoy/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const zaman = req.get('X-Flowoy-Timestamp') || '';
  const imza  = req.get('X-Flowoy-Signature') || '';
  if (Math.abs(Date.now() / 1000 - Number(zaman)) > 300) return res.sendStatus(400);

  const beklenen = 'sha256=' + crypto
    .createHmac('sha256', process.env.FLOWOY_SECRET)
    .update(`${zaman}.${req.body}`)
    .digest('hex');

  const a = Buffer.from(beklenen), b = Buffer.from(imza);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  const olay = JSON.parse(req.body);
  res.sendStatus(200); // önce 200 dön, işi arka planda yap
});

Teslim günlüğü

GET/api/v1/webhooks/{id}/deliveries

"Webhook'um neden gelmedi" sorusunun cevabı. Her denemenin olay adı, HTTP durum kodu, hata metni, kaçıncı deneme olduğu ve süresi kayıtlıdır.

{ "ok": true, "count": 2, "data": [
  { "event": "message.delivered", "statusCode": 200, "ok": true,
    "attempt": 1, "durationMs": 184, "at": "2026-10-31T09:14:22Z" },
  { "event": "message.failed", "statusCode": 502, "ok": false,
    "error": "HTTP 502", "attempt": 3, "durationMs": 10004, "at": "2026-10-31T08:02:10Z" }
] }

Gönderim kapıları

API panelin kısa yolu değil. Her gönderim, panelden yapılan gönderimle aynı kapılardan sırayla geçer; hangisine takıldığınızı yanıttaki kod söyler.

  1. 1
    Anahtar401 unauthorized

    Hesap bağlamı yalnız anahtardan gelir. Anahtarsız hiçbir uç iş yapmaz.

  2. 2
    Plan403 plan_required

    API erişimi plandan kaldırılırsa daha önce üretilmiş anahtarlar da çalışmayı bırakır.

  3. 3
    Hız sınırı429 rate_limited

    Dakikada 120 istek. Retry-After kadar bekleyin.

  4. 4
    Şablon onayı400 template

    Yalnız Meta'da onaylı şablon gönderilir. Taslak ya da reddedilmiş şablon listede bile görünmez.

  5. 5
    Değişken denetimi400 variables

    Eksik ya da fazla anahtar burada yakalanır. Bu denetim izin kapısından önce çalışır ki hatalı bir istek müşterinin sıklık sayacını tüketmesin.

  6. 6
    İzin kapısı403 not_sendable

    Pazarlama izni, opt-out, kara liste ve sıklık sınırı — dördü de veritabanı sorgusunda uygulanır. UTILITY ve AUTHENTICATION şablonları hizmet mesajı sayılır; izin ve sıklık sınırı onlara uygulanmaz.

  7. 7
    Kuyruk202 queued

    Buraya gelen mesaj kuyruğa girer; numaranızın saatlik ve günlük kotası burada uygulanır. Sonuç webhook ile size döner.

Teslim hataları

failed olayındaki errorCode Meta'nın kodunu taşır. Hangisi kalıcı, hangisi beklemeli:

KodAnlamıNe yapmalı
131026Numara WhatsApp'ta yok, kullanıcı yeni koşulları kabul etmemiş ya da uygulaması çok eskiTekrar denemeyin. Üst üste iki kez alan numara otomatik kara listeye alınır; panelden geri açabilirsiniz
13104724 saatlik pencere kapalıSerbest metin yerine şablon gönderin
131049WhatsApp, kullanıcı etkileşimi düşük olduğu için pazarlama mesajını teslim etmediTekrar denemeyin; bu numaraya bir süre pazarlama göndermeyin
130472Kullanıcı Meta'nın deneme grubundaİşlem gerekmez
132000+Şablon parametreleri şablonla uyuşmuyorGET /templates ile beklenen değişkenleri karşılaştırın

Henüz olmayanlar

Yol haritasında olup bugün bulunmayanlar. Entegrasyonunuzu bunlara dayandırmayın.

  • Gelen mesaj geçmişi. message.received olayı anlık gelir, ama geçmiş yazışmalar API'den okunamaz; panelden görülür.
  • Şablon oluşturma. Şablonlar panelden yaratılıp Meta onayına gönderilir.
  • Kampanya tetikleme. Kampanyalar panelden başlatılır; API'den tek tek ya da 25'lik partiler hâlinde gönderim yapılır.
  • Test (sandbox) ortamı. Ayrı bir test hesabı yoktur; denemelerinizi kendi numaranızla yapın.
  • Yetki kapsamları. Anahtar hesabın tüm API uçlarına erişir; uç bazlı kısıtlama yoktur.

Eksik bulduğunuz bir uç mu var? Bize yazın — yol haritasını kullanan ekipler belirliyor.