Paraşüt'te bir cari güncellendiğinde, bir satış faturası resmileştiğinde ya da bir irsaliye silindiğinde bunu kendi sisteminizde anında bilmek istiyorsanız iki yol vardır: API'yi dakikada bir sorgulamak ya da Paraşüt'ün size haber vermesini beklemek. Webhook ikinci yoldur. Belirlediğiniz HTTPS adresine, abone olduğunuz olay gerçekleştiğinde Paraşüt bir POST isteği gönderir; siz de bu isteği doğrulayıp kuyruğa alır, gerekirse kaydın güncel hâlini API'den okursunuz.

Bu yazıda Paraşüt API v4 webhook uçlarını uçtan uca ele alıyoruz: hangi API paketinin gerektiği, client_key kuralları, webhook oluşturma, PATCH ile güncelleme ve silme, X-Client-Key doğrulaması, 30 saniyelik yanıt sınırı, yeniden denemeler, tekrar gelen olayları event_id ile ayıklama, sırayı data.updated_at ile kurma ve en önemlisi düzenli eşitleme. Sonunda doğrudan kullanabileceğiniz bir Node.js/Express alıcı örneği var. API'nin genel yapısını henüz bilmiyorsanız önce Paraşüt entegrasyonu genel rehberine göz atın.

Paraşüt webhook nasıl çalışır?

Paraşüt webhook nasıl çalışır? Webhook kayıtları API istemcisine (client_id) aittir. Aynı firmada her istemci..., Bir istemci, bir firma için her model + action ikilisinden...

Her webhook tek bir model + action ikilisini dinler. Örneğin contact + update webhook'u yalnızca cari güncellemelerinde tetiklenir. Olay gerçekleştiğinde Paraşüt, webhook'un uri adresine Content-Type: application/json ile bir POST isteği gönderir ve isteğe X-Client-Key header'ını ekler. Uç noktanız 2xx dönerse istek teslim edilmiş sayılır.

  • Webhook kayıtları API istemcisine (client_id) aittir. Aynı firmada her istemci yalnızca kendi webhook'larını görür ve yönetir.
  • Bir istemci, bir firma için her model + action ikilisinden yalnızca bir webhook tanımlayabilir. Aynı ikili için ikinci istek 422 döner.
  • model ve action sonradan değiştirilemez; başka bir olayı dinlemek için yeni webhook oluşturursunuz.
  • Webhook uçları, diğer API çağrılarınızla aynı access token ile çağrılır. Token almayı Paraşüt OAuth2 token alma rehberinde anlattık.

Başlamadan önce: paket, yetki ve 403 sebepleri

Webhook uçlarını herkes çağıramaz. Belgeye göre bu uçlar yalnızca Geliştirici Portalı'ndan alınmış ve API paketi webhook içeren bir API istemcisinin access token'ı ile çalışır. Paketiniz webhook içermiyorsa ya da token Geliştirici Portalı'ndaki bir istemciye ait değilse istekler 403 döner. 403'ün sebebini hatanın title değerinden anlarsınız:

title değeriSebep
API client's package does not include webhooksAPI paketiniz webhook içermiyor
Webhooks are only available to Developer Portal API clientsAccess token Geliştirici Portalı istemcisine ait değil
ForbiddenKullanıcının gerekli Ayarlar yetkisi yok
User has no 3rd party integration permission for company: 123Kullanıcının üçüncü taraf entegrasyon yetkisi yok
API client has no access to company: 123API istemcisinin bu firmaya erişimi yok

Yetki tarafında, webhook'ları listelemek ve görüntülemek için Ayarlar okuma, oluşturmak, güncellemek ve silmek için Ayarlar tam yetkisi gerekir. Token'ın ait olduğu kullanıcının firmada üçüncü taraf entegrasyon yetkisi de olmalıdır. 403 bir yetki ya da paket sorunudur; otomatik yeniden denemek işe yaramaz. Hata yönetimini Paraşüt API hata kodları ve rate limit yazısında ayrıntılı ele aldık.

Desteklenen olaylar (model ve action)

Belgedeki tabloya göre şu ikililer desteklenir. data.data sütunu, gövdede gelen kaydın JSON:API tipidir; data.included ise kayıtla birlikte gönderilen ilişkili kaynaklardır.

modelcreateupdatedeletedata.datadata.included
shipment_document (İrsaliye)VarVarVarshipment_documentsstock_movements
stock_movement (Stok hareketi)VarVarVarstock_movementsproduct
product (Ürün)VarVarVarproductsYok
contact (Müşteri / Tedarikçi)VarVarVarcontactscontact_people (delete olayında yok)
sales_invoice (Satış faturası)VarVarYoksales_invoicesdetails, details.product, contact, active_e_document, failed_e_invoice
stock_update (Stok güncelleme)VarYokYokstock_updatesdetails, details.product, details.warehouse
company (Firma)YokVarYokYok (düz gövde)Yok
purchase_bill (Fiş / Fatura)VarVarYokpurchase_billsdetails, details.product, supplier
inbound_e_invoice (Gelen e-Fatura)VarYokYoke_invoicesYok

Her kayıt için gönderilmeyen olaylar

  • sales_invoice update: yalnızca e-Fatura ya da e-Arşiv belgesi olan faturalar için gönderilir. e-SMM'si olan faturalar, teklifler ve tekrarlayan şablonlar (estimate, recurring_invoice, recurring_estimate, recurring_export) için gönderilmez. create bu türler için de gelir; gerekiyorsa item_type ile ayıklayın.
  • purchase_bill: gelen bir e-Faturaya bağlı fiş/faturalar ve recurring_purchase_bill şablonları için create ve update gönderilmez. Gelen e-Faturaları inbound_e_invoice ile izleyin.
  • stock_movement: stok güncellemesinden ve depo transferinden doğan hareketler için hiçbir olay gönderilmez. Stok güncellemeleri için stock_update kullanın; depo transferlerinin webhook'u yoktur.
  • Tabloda delete desteği olmayan modellerde silmeyi ancak API'den eşitleme yaparak fark edersiniz.

Adım 1: HTTPS uç noktanızı hazırlayın

uri mutlaka https:// ile başlamalı ve en fazla 2048 karakter olmalıdır. http adresler kabul edilmez; özel ağ, localhost ve bağlantı-yerel (link-local) adreslere işaret eden bir uri oluşturma ve güncellemede 422 ile reddedilebilir. Ayrıca Paraşüt yönlendirmeleri takip etmez: https://site.com/webhooks adresinizin www'ya ya da sondaki / için başka bir adrese yönlendirmesi, olayın teslim edilmemesi demektir ve 3xx yanıtlar yeniden denenmez.

bash
# Uç noktanız 3xx dönmemeli. X-Client-Key olmadan 401 gibi bir kod beklenir.
curl -sS -o /dev/null -w "%{http_code}\n" -X POST https://ornek-firma.com/webhooks/parasut \
  -H "Content-Type: application/json" \
  -d '{}'

İstekleri kaynak IP'ye göre de kısıtlamak isterseniz, belgeye göre Paraşüt'ün çıkış IP adreslerini destek ekibinden talep edebilirsiniz. Alıcıyı TLS sertifikalı bir alan adının arkasında çalıştırmak için Cloudflare kurulumu ya da kendi sunucunuzda bir ters vekil kullanabilirsiniz.

Adım 2: client_key üretin ve saklayın

Adım 2: client_key üretin ve saklayın: Tam 64 karakter olmalı ve yalnızca A-Z, a-z, 0-9 içermelidir (desen..., API yanıtlarında hiçbir zaman dönmez. Kaybederseniz Paraşüt'ten...

client_key, her webhook isteğinde X-Client-Key header'ı olarak gelen paylaşılan gizli değerdir. Kurallar nettir:

  • Tam 64 karakter olmalı ve yalnızca A-Z, a-z, 0-9 içermelidir (desen: ^[A-Za-z0-9]{64}$). UUID gibi -, _, +, /, = içeren değerler 422 döner.
  • API yanıtlarında hiçbir zaman dönmez. Kaybederseniz Paraşüt'ten geri alamazsınız; yeni bir değerle güncellersiniz.
  • Silinemez, yalnızca değiştirilebilir. Boş ya da null göndermek 422 döner.
bash
openssl rand -hex 32
# ya da Node.js ile:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

openssl rand -hex 32 64 karakterlik onaltılık bir metin üretir ve desene uyar. Değeri kodun içine değil, ortam değişkenine ya da bir gizli anahtar yöneticisine koyun; ayrıntılar için .env dosyası en iyi pratikleri yazısına bakabilirsiniz.

Adım 3: Webhook oluşturun (POST /webhooks)

Webhook gövdesinde dikkat edilecek en önemli istisna şudur: Paraşüt'te neredeyse tüm kaynaklarda data.type çoğuldur, ama webhook'ta tekil webhook yazılır. webhooks gönderirseniz 422 alırsınız.

bash
curl -X POST "https://api.parasut.com/v4/{company_id}/webhooks" \
  -H "Authorization: Bearer $PARASUT_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhook",
      "attributes": {
        "model": "sales_invoice",
        "action": "update",
        "uri": "https://ornek-firma.com/webhooks/parasut",
        "client_key": "<64_KARAKTERLIK_CLIENT_KEY>"
      }
    }
  }'
AlanKural
data.typeZorunlu, tekil webhook
attributes.modelZorunlu; shipment_document, stock_movement, product, contact, sales_invoice, stock_update, company, purchase_bill, inbound_e_invoice
attributes.actionZorunlu; create, update veya delete (modelin desteklediği bir değer)
attributes.uriZorunlu; https ile başlar, en fazla 2048 karakter
attributes.client_keyZorunlu; tam 64 karakter, yalnızca harf ve rakam; yanıtta dönmez

Başarılı istek 201 ile oluşturulan webhook'u döner. Yanıtta model, action, uri, company_id, created_at ve updated_at bulunur; client_key bulunmaz.

json
{
  "data": {
    "id": "42",
    "type": "webhook",
    "attributes": {
      "model": "sales_invoice",
      "action": "update",
      "uri": "https://ornek-firma.com/webhooks/parasut",
      "company_id": 123,
      "created_at": "2025-01-10T10:00:00.000Z",
      "updated_at": "2025-01-10T10:00:00.000Z"
    }
  }
}

Webhook'ları listeleme, güncelleme ve silme

İşlemUç noktaBaşarılı yanıt
ListeleGET /{company_id}/webhooks200 (sayfalama yok)
OluşturPOST /{company_id}/webhooks201
GörüntüleGET /{company_id}/webhooks/{id}200 (başka istemcininki 404)
GüncellePATCH /{company_id}/webhooks/{id}200
SilDELETE /{company_id}/webhooks/{id}204, gövde yok

Güncelleme PATCH ile yapılır ve yalnızca uri ile client_key değiştirilebilir; gönderdiğiniz alanlar değişir, göndermedikleriniz korunur. İstekte model ya da action göndermeyin; mevcut değerden farklı bir değer 422 döner. Anahtarı döndürmek (rotation) için:

bash
curl -X PATCH "https://api.parasut.com/v4/{company_id}/webhooks/{id}" \
  -H "Authorization: Bearer $PARASUT_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhook",
      "attributes": { "client_key": "<YENI_64_KARAKTERLIK_CLIENT_KEY>" }
    }
  }'

Gelen isteğin gövdesi

Zarf alanları tüm modellerde aynıdır: event_id, company_id, resource (webhook'un model değeri), action, event_date ve data.

http
POST /webhooks/parasut HTTP/1.1
Content-Type: application/json
X-Client-Key: <client_key>

{
  "event_id": "9f2c4e7a1b3d5f60718293a4b5c6d7e8",
  "company_id": 123,
  "resource": "contact",
  "action": "update",
  "event_date": "2025-01-10T10:55:00Z",
  "data": {
    "id": 456,
    "company_id": 123,
    "created_at": "2025-01-09T08:00:00.000Z",
    "updated_at": "2025-01-10T10:54:59.123Z",
    "data": { "id": "456", "type": "contacts", "attributes": { "name": "..." } }
  }
}
AlanNe işe yarar
event_idOlayın bu webhook için kimliği; tekrar gönderimlerde değişmez. Biçimine güvenmeyin, yalnızca eşitlik için karşılaştırın
event_dateParaşüt'ün olayı işlediği zaman (UTC). Kaydın değiştiği an değildir, sıralama için kullanmayın
data.idKayıt ID, sayı (integer). data.data.id ise metin (string)
data.updated_atKaydın son değiştiği zaman; sıralama için bunu kullanın
data.dataKaydın JSON:API kaynak nesnesi (id, type, attributes, relationships)
data.includedİlişkili kaynaklar; modele göre değişir
  • company olayında data JSON:API değildir; firmanın alanlarını (name, tax_number, e_invoicing_enabled, credit_balance gibi) düz olarak içerir.
  • delete olayında data, kaydın silinmeden hemen önceki hâlidir; birlikte silinen ilişkili kayıtlar data.included içinde yer almayabilir.
  • Yeni alanlar, yeni enum değerleri ve yeni header'lar eklenebilir. Tanımadığınız her şeyi yok sayın, gövdeye katı şema doğrulaması uygulamayın.

Node.js/Express ile webhook alıcısı

Aşağıdaki alıcı belgedeki akışı izler: doğrula, kuyruğa yaz, hemen 2xx dön. Asıl iş (tekrar ayıklama, sıra kontrolü, API'den okuma) ayrı bir işçide yapılır. jobs ve db sizin kalıcı kuyruğunuz ve veri katmanınızdır; bir veritabanı tablosu ya da Redis tabanlı bir kuyruk olabilir. Express temelleri için Node.js REST API geliştirme yazımıza bakabilirsiniz.

javascript
const crypto = require('crypto');
const express = require('express');

const app = express();

// Geçerli anahtar + (varsa) döndürme sırasında eski anahtar
const KEYS = [process.env.PARASUT_WEBHOOK_CLIENT_KEY, process.env.PARASUT_WEBHOOK_CLIENT_KEY_OLD]
  .filter(Boolean)
  .map((k) => Buffer.from(k, 'utf8'));

if (!KEYS.length || KEYS.some((k) => k.length !== 64)) {
  throw new Error('client_key 64 karakter olmalı');
}

function hasValidClientKey(req) {
  const got = Buffer.from(req.get('X-Client-Key') || '', 'utf8');
  // timingSafeEqual farklı uzunlukta hata fırlatır; önce uzunluğu kontrol edin
  return KEYS.some((k) => got.length === k.length && crypto.timingSafeEqual(got, k));
}

// Varsayılan 100kb sınırı büyük gövdelerde 413 döndürür; 413 yeniden denenmez
app.post('/webhooks/parasut', express.json({ limit: '5mb' }), async (req, res) => {
  if (!hasValidClientKey(req)) {
    return res.sendStatus(401); // Belge kod belirtmez; 401 önerilir
  }
  try {
    await jobs.enqueue(req.body); // Yalnızca kalıcı kuyruğa yaz
    return res.sendStatus(200);   // 30 saniye sınırının çok altında yanıt ver
  } catch (err) {
    return res.sendStatus(503);   // 5xx yeniden denenir
  }
});

app.listen(process.env.PORT || 3000);

İşçi tarafında tekrar ve sıra kontrolü şöyle yapılabilir:

javascript
jobs.process(async (event) => {
  // event_id webhook başınadır; firma + model + action ile birlikte saklayın
  const eventKey = [event.company_id, event.resource, event.action, event.event_id].join(':');
  if (await db.processedEvents.has(eventKey)) return; // tekrar

  const key = event.resource + ':' + event.data.id;
  const record = await db.records.get(key);

  if (record && record.deleted) {
    // Silinmiş kayda sonradan gelen create/update işlenmez
  } else if (event.action === 'delete') {
    await db.records.set(key, { deleted: true });
  } else if (!record || Date.parse(event.data.updated_at) >= Date.parse(record.updated_at)) {
    // Daha eski updated_at taşıyan isteği yenisinin üzerine yazmayın
    await db.records.set(key, { updated_at: event.data.updated_at });
    await syncFromApi(event.company_id, event.resource, event.data.id); // güncel hâli API'den oku
  }

  await db.processedEvents.add(eventKey);
});

Teslimat, zaman aşımı ve yeniden deneme

Teslimat, zaman aşımı ve yeniden deneme: Zaman aşımı süreleri, deneme sayısı ve aralıklar zamanla değişebilir; koda..., Sürekli başarısız olan webhook'lar devre dışı...

Bağlantı 10 saniyede kurulamazsa ya da yanıt 30 saniyede gelmezse istek başarısız sayılır. Bu yüzden isteğin içinde API çağrısı, PDF indirme ya da e-posta gönderme gibi işler yapmayın.

Uç noktanızın yanıtıNe olur
2xxTeslim edildi
5xx, 408, 429, zaman aşımı, bağlantı ya da SSL hatasıArtan aralıklarla 10 kereye kadar, yaklaşık 4-5 saat boyunca yeniden denenir; sonra o olay için istek gönderilmez
Diğer 4xx ve 3xxYeniden denenmez; yönlendirmeler takip edilmez
  • Zaman aşımı süreleri, deneme sayısı ve aralıklar zamanla değişebilir; koda sabit varsayım gömmeyin.
  • Sürekli başarısız olan webhook'lar devre dışı bırakılabilir. Webhook kaydında bir durum alanı belgelenmediği için bunu izlemenin resmî bir yolu tanımlı değildir; eşitleme işi bu açığı da kapatır.
  • Geçici bir sorunda bilerek 503 dönerseniz Paraşüt tekrar dener; 400 ya da 404 dönerseniz o olay kaybolur.

Sıra ve tekrar: event_id ve data.updated_at

Webhook istekleri işlemlerin gerçekleştiği sırayla gelmeyebilir. Bir kaydın update isteği create isteğinden önce ulaşabilir; art arda iki güncellemenin ikincisi önce gelebilir. Belgedeki kurallar:

  • Aynı kayda (resource + data.id) ait istekleri data.updated_at ile karşılaştırın; daha eski olanı yenisinin üzerine işlemeyin.
  • Bir kayıt için delete aldıktan sonra aynı kayda gelen create ve update isteklerini işlemeyin; silinmiş kaydı yeniden oluşturmayın.
  • event_date ile sıralamayın.
  • Aynı olay birden fazla kez gelebilir; event_id ile ayıklayın.
  • Kaydın güncel hâline ihtiyacınız varsa API'den okuyun, örneğin GET /{company_id}/contacts/{id}.

Periyodik eşitleme neden şart?

Belge açıkça söylüyor: webhook istekleri en iyi çaba ilkesiyle gönderilir, her değişiklik için bir istek garanti edilmez. Kısa sürede art arda değişen bir kayıt için yalnızca son hâl gönderilebilir; uç noktanız yeniden deneme süresinden uzun kapalı kalırsa o olaylar hiç gelmez. Bazı modellerde (sales_invoice, purchase_bill gibi) silme olayı da yoktur.

Bu yüzden webhook'u bir değişiklik bildirimi olarak görün ve verilerinizi düzenli aralıklarla API'den eşitleyin. Pratik bir kurgu: gecelik ya da saatlik bir zamanlanmış görev, ilgili liste uçlarını sayfa sayfa gezer ve kendi kopyanızla karşılaştırır. Çoğu liste ucunda page[size] en fazla 25'tir; sayfalama ve filtre kurallarını filtreleme, sayfalama ve include yazısında bulabilirsiniz. Görevi kurmak için cron job rehberi ya da systemd servis dosyası rehberi işinize yarar; eşitleme sırasında rate limit stratejilerine uymayı unutmayın.

Webhook uçlarının hata yanıtları

DurumNe zaman döner
400Gövdede data yok, data ya da data.attributes nesne değil, ya da gövde geçerli JSON değil
401Access token geçersiz, süresi dolmuş ya da kullanıcının bu company_id için erişimi yok
402Firmanın Paraşüt kullanım süresi sona ermiş; errors dizisi metin içerir
403Paket webhook içermiyor, token portal istemcisine ait değil ya da yetki eksik
404Webhook bulunamadı ya da başka bir API istemcisine ait
422data.type webhook değil, client_key ya da uri geçersiz, ikili desteklenmiyor, ikili zaten var, model/action değiştirilmek istendi
500, 503Paraşüt tarafında geçici sorun; daha sonra yeniden deneyin

Doğrulama hatalarında errors[].source.pointer hatalı alanı gösterir (ör. /data/attributes/uri). Hata mesajları İngilizcedir ve değişebilir; programatik ayrım için durum kodunu ve source.pointer değerini kullanın. Geçersiz JSON gövdesi ise JSON:API olmayan {"error": "Invalid JSON"} yanıtıyla 400 döner.

Sık Sorulan Sorular

Paraşüt webhook'unu localhost'ta test edebilir miyim?

Hayır. uri internetten erişilebilen bir HTTPS adresi olmalıdır; localhost, özel ağ ve link-local adresler reddedilebilir ve bu adreslere istek gönderilmeyebilir. Geliştirme için de alan adı ve geçerli sertifikası olan, dışarıdan erişilebilen bir adres kullanın.

X-Client-Key eşleşmezse hangi kodu dönmeliyim?

Belge bir durum kodu belirtmez; isteği reddetmeniz yeterlidir. Önerilen değer 401'dir. 408 ve 429 dışındaki 4xx yanıtlar yeniden denenmediği için reddettiğiniz istek tekrar gelmez.

Webhook ile satış faturasının e-Fatura sonucunu takip edebilir miyim?

Kısmen. sales_invoice update olayı e-Fatura ya da e-Arşiv belgesi olan faturalar için gelir ve PDF oluştuğunda 2 saat geçerli pdf_url taşır. Ancak teslimat garanti edilmediği için resmileştirme işinin sonucunu her zaman GET /{company_id}/trackable_jobs/{id} ile doğrulayın. e-SMM'si olan faturalar için update olayı gelmez.

client_key'i kaybettim, ne yapmalıyım?

API client_key değerini hiçbir zaman döndürmez. Yeni bir 64 karakterlik değer üretip PATCH /{company_id}/webhooks/{id} ile güncelleyin; sonraki istekler yeni değerle gelir.

Aynı olay için birden çok webhook kurabilir miyim?

Aynı API istemcisi, aynı firmada her model + action ikilisi için yalnızca bir webhook tanımlayabilir. Olayı birden fazla sisteme dağıtmak istiyorsanız tek alıcıda kuyruğa alıp oradan dağıtın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Paraşüt entegrasyonu genel rehberi · Paraşüt OAuth2 token alma · Paraşüt e-Fatura ve e-Arşiv gönderme · Paraşüt API hata kodları ve rate limit · Paraşüt API irsaliye ve e-İrsaliye · E-ticaret siparişini Paraşüt'e aktarma · REST API güvenlik rehberi