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?

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+actionikilisinden yalnızca bir webhook tanımlayabilir. Aynı ikili için ikinci istek 422 döner. modelveactionsonradan 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ğeri | Sebep |
|---|---|
| API client's package does not include webhooks | API paketiniz webhook içermiyor |
| Webhooks are only available to Developer Portal API clients | Access token Geliştirici Portalı istemcisine ait değil |
| Forbidden | Kullanıcının gerekli Ayarlar yetkisi yok |
| User has no 3rd party integration permission for company: 123 | Kullanıcının üçüncü taraf entegrasyon yetkisi yok |
| API client has no access to company: 123 | API 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.
| model | create | update | delete | data.data | data.included |
|---|---|---|---|---|---|
| shipment_document (İrsaliye) | Var | Var | Var | shipment_documents | stock_movements |
| stock_movement (Stok hareketi) | Var | Var | Var | stock_movements | product |
| product (Ürün) | Var | Var | Var | products | Yok |
| contact (Müşteri / Tedarikçi) | Var | Var | Var | contacts | contact_people (delete olayında yok) |
| sales_invoice (Satış faturası) | Var | Var | Yok | sales_invoices | details, details.product, contact, active_e_document, failed_e_invoice |
| stock_update (Stok güncelleme) | Var | Yok | Yok | stock_updates | details, details.product, details.warehouse |
| company (Firma) | Yok | Var | Yok | Yok (düz gövde) | Yok |
| purchase_bill (Fiş / Fatura) | Var | Var | Yok | purchase_bills | details, details.product, supplier |
| inbound_e_invoice (Gelen e-Fatura) | Var | Yok | Yok | e_invoices | Yok |
Her kayıt için gönderilmeyen olaylar
sales_invoiceupdate: 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.createbu türler için de gelir; gerekiyorsaitem_typeile ayıklayın.purchase_bill: gelen bir e-Faturaya bağlı fiş/faturalar verecurring_purchase_billşablonları içincreateveupdategönderilmez. Gelen e-Faturalarıinbound_e_invoiceile izleyin.stock_movement: stok güncellemesinden ve depo transferinden doğan hareketler için hiçbir olay gönderilmez. Stok güncellemeleri içinstock_updatekullanın; depo transferlerinin webhook'u yoktur.- Tabloda
deletedesteğ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.
# 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

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-9iç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
nullgöndermek 422 döner.
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.
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>"
}
}
}'| Alan | Kural |
|---|---|
| data.type | Zorunlu, tekil webhook |
| attributes.model | Zorunlu; shipment_document, stock_movement, product, contact, sales_invoice, stock_update, company, purchase_bill, inbound_e_invoice |
| attributes.action | Zorunlu; create, update veya delete (modelin desteklediği bir değer) |
| attributes.uri | Zorunlu; https ile başlar, en fazla 2048 karakter |
| attributes.client_key | Zorunlu; 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.
{
"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
| İşlem | Uç nokta | Başarılı yanıt |
|---|---|---|
| Listele | GET /{company_id}/webhooks | 200 (sayfalama yok) |
| Oluştur | POST /{company_id}/webhooks | 201 |
| Görüntüle | GET /{company_id}/webhooks/{id} | 200 (başka istemcininki 404) |
| Güncelle | PATCH /{company_id}/webhooks/{id} | 200 |
| Sil | DELETE /{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:
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.
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": "..." } }
}
}| Alan | Ne işe yarar |
|---|---|
| event_id | Olayı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_date | Paraşüt'ün olayı işlediği zaman (UTC). Kaydın değiştiği an değildir, sıralama için kullanmayın |
| data.id | Kayıt ID, sayı (integer). data.data.id ise metin (string) |
| data.updated_at | Kaydın son değiştiği zaman; sıralama için bunu kullanın |
| data.data | Kaydın JSON:API kaynak nesnesi (id, type, attributes, relationships) |
| data.included | İlişkili kaynaklar; modele göre değişir |
companyolayındadataJSON:API değildir; firmanın alanlarını (name,tax_number,e_invoicing_enabled,credit_balancegibi) düz olarak içerir.deleteolayındadata, kaydın silinmeden hemen önceki hâlidir; birlikte silinen ilişkili kayıtlardata.includediç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.
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:
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

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 |
|---|---|
| 2xx | Teslim 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 3xx | Yeniden 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 istekleridata.updated_atile karşılaştırın; daha eski olanı yenisinin üzerine işlemeyin. - Bir kayıt için
deletealdıktan sonra aynı kayda gelencreateveupdateisteklerini işlemeyin; silinmiş kaydı yeniden oluşturmayın. event_dateile sıralamayın.- Aynı olay birden fazla kez gelebilir;
event_idile 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ı
| Durum | Ne zaman döner |
|---|---|
| 400 | Gövdede data yok, data ya da data.attributes nesne değil, ya da gövde geçerli JSON değil |
| 401 | Access token geçersiz, süresi dolmuş ya da kullanıcının bu company_id için erişimi yok |
| 402 | Firmanın Paraşüt kullanım süresi sona ermiş; errors dizisi metin içerir |
| 403 | Paket webhook içermiyor, token portal istemcisine ait değil ya da yetki eksik |
| 404 | Webhook bulunamadı ya da başka bir API istemcisine ait |
| 422 | data.type webhook değil, client_key ya da uri geçersiz, ikili desteklenmiyor, ikili zaten var, model/action değiştirilmek istendi |
| 500, 503 | Paraşü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