Geliştiriciler
Webhooks referansı
Payload şeması, etkinlik türleri, imzalama, yeniden deneme davranışı ve Zapier ile Make için REST abonelik uç noktaları.
Kurulum kılavuzunu mu arıyorsunuz?
Bu sayfa, payload sözleşmesini ve REST abonelik API’sini belgeler. Formunuz için UI’de özel bir webhook kurmak için bkz. Özel webhook’lar.
İstek
POST <your-url> ile Content-Type: application/json.
Başlıklar
Content-Type: application/jsonX-formbase-Signature: t={timestamp},sha256={hex}— imzalama gizli anahtarı ayarlandığında eklenir (aşağıya bakın)X-formbase-Event-IdveX-formbase-Event-Type— gövdedekiidvetypeile aynı değerler, böylece ayrıştırmadan önce yinelenenleri ayıklayıp yönlendirebilirsiniz. Bir istek geri çağırması da aynı ikisini gönderir.Kurulum sırasında eklediğiniz özel başlıklar. Verildiği gibi birleştirilirler, üzerine yazılamayan
Content-Typehariç.webhooks.createile oluşturulan yerel aboneliklerin özel başlığı yoktur.
Etkinlik türleri
Abonelik etkinlikleri
Bir webhook entegrasyonu kurduğunuzda, hangi etkinliğin teslimatları tetikleyeceğini seçersiniz:
| Etkinlik | Ne zaman tetiklenir |
|---|---|
| submission_created | Bir katılımcı formu tamamlayıp gönderdiğinde. Bu varsayılan seçenektir. |
| submission_updated | Form gönderim sonrası düzenlemeye izin veriyorsa, bir katılımcı daha önce gönderdiği bir gönderimi düzenlediğinde. |
| submission_abandoned | Bir taslak submission, yapılandırılan süre boyunca atıl kaldığında. Kısmi submission takibi gerektirir (Pro). |
| request_completed | Bir alıcı formdaki bir isteği tamamlar. İstek bloğunu ve yanıtları taşır. |
| request_expired | Formdaki bir isteğin süresi, alıcı tamamlamadan dolar. Yalnızca istek bloğu. |
| request_canceled | Formdaki bir istek iptal edilir. Yalnızca istek bloğu. |
Üç request_* etkinliği webhooks.create ile abone olunur ve Zapier, Make ile n8n için formbase uygulamalarının
dinlediği şey budur. Her biri bir istek geri çağırmasının gönderdiği aynı zarfı, aboneliğin kendi
sırrıyla imzalanmış olarak teslim eder. Test istekleri hiçbir zaman bir aboneliğe ulaşmaz ve requests.replayCallback yalnızca
geri çağırmayı yeniden gönderir. Bir abonelik, bir etkinlik: submission_created genel bağlantı trafiğidir,
request_completed ise istek trafiğidir; bu yüzden tamamlanan bir istek submission_created’ı hiçbir zaman
tetiklemez ve her iki aboneliğe de sahip bir form, bir tamamlanma için tek teslimat alır. Bir düzenleme yalnızca bir
submission_updated aboneliğine ulaşır, asla bir submission_created aboneliğine değil. Form ayarlarında
yapılandırdığınız webhook’ta etkinlik seçimi yoktur: ilk gönderimleri ve düzenlemeleri aynı şekilde alır, type ile ayırt
edilir.
Payload etkinlik türleri
JSON gövdesindeki type alanı ne olduğunu belirtir:
submission.completed— yeni ve tamamlanmış bir submissionsubmission.updated— mevcut bir submission düzenlendisubmission.abandoned— bir submission taslağı yapılandırılmış boşta kalma süresinden sonra terk edildi
Bir istek geri çağırması ve bir request_* aboneliği, aynı zarfı
request.completed, request.expired ve request.canceled ile kullanır, böylece tek bir ayrıştırıcı
altısını da okur.
Payload yapısı
{
"id": "evt_abc123",
"type": "submission.completed",
"createdAt": "2026-04-25T12:34:56.000Z",
"apiVersion": "2026-09-24",
"test": false,
"data": {
"form": { "id": "frm_...", "name": "Contact", "snapshotId": "snp_..." },
"submission": {
"id": "sub_...",
"respondentEmail": "alice@example.com",
"submittedAt": "2026-04-25T12:34:56.000Z",
"updatedAt": null,
"editCount": 0,
"pdfUrl": null,
"language": "en"
},
"answers": {
"email": "alice@example.com",
"plan": "pro",
"attendees": [{ "attendee_name": "Grace Hopper" }, { "attendee_name": "Alan Turing" }]
},
"display": {
"email": "alice@example.com",
"plan": "Pro",
"attendees": "Grace Hopper, Alan Turing"
}
}
}| Anahtar | Ne olduğu |
|---|---|
| id | Etkinlik kimliği. Yeniden denemeler aynısını kullanır — yinelenenleri bunun üzerinden ayıklayın. |
| type | Yukarıdaki altı etkinlik türünden biri. |
| createdAt | Etkinliğin sıraya alındığı an, bu teslimat denemesinin yapıldığı an değil. Yeniden denemeler arasında sabit kalır. |
| apiVersion | Payload sözleşmesi, bir tarih olarak. Bir anahtar kaldırıldığında, yeniden adlandırıldığında veya anlamı değiştiğinde değişir. Yeni anahtarlar değişiklik olmadan eklenir. |
| test | Bir örnek veya test teslimatı için, ve test modunda oluşturulan bir istek için true. Her zaman bulunur. |
| data.form.snapshotId | Katılımcının yanıtladığı yayınlanmış sürüm. Alan anahtarları, başlıklar ve türler her sürüm için sabittir. |
| data.submission.updatedAt | Katılımcının gönderimi en son ne zaman düzenlediği; ilk düzenlemeye kadar null. |
| data.submission.editCount | Katılımcının gönderimi gönderdikten sonra kaç kez düzenlediği: submission.completed üzerinde 0, ilk düzenlemede 1. |
| data.answers | Her yanıt, alan anahtarıyla anahtarlanmış. Her yanıt yalnızca bir kez görünür. |
| data.display | Aynı anahtarlar altında her yanıtın insan tarafından okunabilir metni. |
| data.schema | İsteğe bağlı: webhook her etkinlikle birlikte gönderecek şekilde ayarlandıysa alan listesi (key, title, type, group, ve etiketleriyle birlikte seçenek ya da satır ve sütun anahtarları). |
answers ve display
answers, yayınlama sırasında sabitlenen alan anahtarlarıyla anahtarlanan düz { field key: value }
nesnesidir. Bir workflow dallandığında veya bir değeri sakladığında bunu okuyun: answers.email, gezilecek bir dizi yok. Bir
seçim yanıtı, seçilen seçeneğin anahtarıdır — fields.list’in o seçenek için listelediği key —
bu yüzden katılımcı hangi dilde yanıtlarsa yanıtlasın aynıdır. Bir tarih ISO dizesi, bir sayı sayı, çoklu seçim ise seçenek
anahtarlarından oluşan bir dizidir. Yanıtlanmamış alanlar dışarıda bırakılır, asla null olarak gönderilmez.
display, aynı anahtarları insan tarafından okunabilir metinle taşır: anahtar yerine seçeneğin etiketi, biçimlendirilmiş bir
tarih, birleştirilmiş bir liste. Değeri bir kişi göreceğinde bunu okuyun — bir Slack mesajı, bir e-tablo hücresi, bir e-posta.
Yinelenen bir grup answers içinde bir kez, grubun kendi alan anahtarı altında, her üyenin kendi alan anahtarıyla
anahtarlanmış satır nesnelerinden oluşan bir dizi olarak görünür — yukarıdaki answers.attendees[0].attendee_name — ve
display içinde satırların birleştirildiği tek bir satır olarak. Bir üye asla üst düzeye taşınmaz.
Hesaplanan alanlar her iki eşlemede de, hesaplanan alanın adını anahtarı olarak
kullanarak görünür (answers.total).
Randevular ve ödemeler
Bir Randevu planlama sorusu ve bir Ödeme alanı, answers içinde sorunun alan anahtarı altında birer nesne ve
display içinde tek satırlık bir metin taşır. Zamanlar ISO anlarıdır; böylece bir e-tablo veya bir workflow, katılımcı hangi
dilde yanıtlarsa yanıtlasın onları ayrıştırabilir:
{
"book_a_call": {
"status": "confirmed",
"start": "2026-09-29T07:00:00.000Z",
"end": "2026-09-29T07:30:00.000Z",
"timeZone": "Europe/Oslo",
"attendee": { "name": "Grace Hopper", "email": "grace@example.com" },
"meetingUrl": "https://app.cal.com/video/...",
"provider": "cal.com",
"providerBookingId": "...",
"eventTitle": "Intro call"
},
"pay_the_fee": {
"status": "paid",
"amount": 40,
"currency": "USD",
"amountRefunded": 0,
"receiptUrl": "https://pay.stripe.com/receipts/...",
"paidAt": "2026-09-24T10:12:00.000Z",
"refundedAt": null,
"disputedAt": null,
"provider": "stripe",
"providerPaymentIntentId": "pi_..."
}
}Bir randevunun status değeri confirmed, rescheduled, cancelled, rejected
veya no_show olur. Bir ödemeninki paid, partially_refunded, refunded veya
disputed olur ve amount para biriminin ana birimindedir: 40, 40,00 $ demektir. Cal.com gönderimden
sonra bir randevuyu taşıdığında veya Stripe bir ödemeyi iade ettiğinde formbase nesneyi günceller, böylece submissions.list
ve sonraki olaylar güncel durumu gösterir; değişiklik için yeni bir olay gönderilmez.
apiVersion 2026-09-24 öncesinde bir randevu answers içinde tek bir cümle olarak gönderiliyordu ve
bir ödeme hiç gönderilmiyordu.
Webhook’un alan eşlemesi her iki eşlemeye de aynı anda uygulanır: “seçili” alanları seçin, diğerleri dışarıda bırakılır; bir alanın
sütununu yeniden adlandırın, yeni ad hem answers hem de display içinde onun anahtarı olur. Alan anahtarları var
olmadan önce yayınlanmış bir alan, kendi eleman kimliği altında gider; okunabilir bir anahtar vermek için formu yeniden yayınlayın.
Alan başlıkları ve türleri
Etkinlik, her alanın başlığını ve türünü tekrarlamaz. Bunları, data.form.snapshotId başına sabit olan
fields.list’ten okuyun; böylece alan listesini önbelleğe alıp yalnızca snapshot kimliği değiştiğinde yeniden çekebilirsiniz.
İkinci bir çağrı yapamayan bir alıcı, webhook ayarlarında Alan listesini her etkinlikle gönder seçeneğini açabilir;
etkinlik o zaman alan başına bir kayıt taşıyan data.schema’yı içerir. Bir seçim sorusu options’ını, bir matris
rows ve columns’ını listeler, her biri { key, label } olarak — böylece answers
içindeki anahtarlar ikinci bir çağrı olmadan etiketlere çözümlenir:
[
{ "key": "email", "title": "Email", "type": "email", "group": null },
{ "key": "plan", "title": "Plan", "type": "radio", "group": null, "options": [{ "key": "free", "label": "Free" }, { "key": "pro", "label": "Pro" }] },
{ "key": "attendee_name", "title": "Attendee name", "type": "text", "group": "attendees" }
]İstek bloğu
Form ayarlarında yapılandırılan özel webhook’ta, bir isteği
yanıtlayan bir gönderi, data içinde bir ekstra nesne taşır: request. Her genel bağlantı gönderiminde bulunmaz,
bu yüzden onun varlığı bu alıcının iki kanalı birbirinden ayırt etme yoludur. Bir Zapier, Make veya n8n aboneliği onu hiçbir zaman görmez:
istek trafiği bir aboneliğe request.completed olarak ulaşır, bu da istek bloğunun tamamını taşır.
"request": {
"id": "kd7...",
"externalId": "run-42",
"metadata": { "crm_record": "rec_88" }
}id— bu gönderinin yanıtladığı istek. Tam görünüm içinrequests.get’e geçirin.externalIdvemetadata—requests.createüzerinde tam olarak sağladığınız şekliyle kendi kayıt tutma bilgileriniz. Her biri yalnızca ayarlanmışsa bulunur.
Webhook’lar geri çağırma değildir
Bir gönderim webhook’u bir gönderide tetiklenir; istek bloğu yalnızca yanıtladığı isteği adlandırır. Bir
geri çağırma bir istek sona erdiğinde — tamamlandığında, süresi dolduğunda veya iptal edildiğinde —
tetiklenir ve context ile outcome taşır. Süre dolumu ve iptalin gönderimi yoktur, bu yüzden onlar için hiçbir
zaman bir gönderim webhook’u tetiklenmez. Bir isteğin sona erdiğini bir geri çağırma URL’si olmadan duymak için,
webhooks.create üzerinden request_completed, request_expired veya request_canceled’a
abone olun.
Gönderim PDF’i
data.submission.pdfUrl, gönderim PDF’ine bir bağlantıdır. Özel webhook’u veya Zapier, Make ya da n8n aboneliği olan bir form,
her gönderimin PDF’ini saklar; bu yüzden o formun olayları bağlantıyı taşır. Yalnızca gönderimin PDF’i yoksa null olur.
Gönderim dili
data.submission.language katılımcının gönderimi yaptığı andaki dilin BCP-47 kodudur (çevrilmiş formlar için). Tek dilli
formlar için null değerini alır. Ayrı bir sorgu yapmadan katılımcının diline göre yönlendirme veya dallanma yapmak için bu
değeri kullanın.
Terk edilmiş submission payload’ları
submission_abandoned etkinliğine abone olduğunuzda, formbase her saat atıl taslakları kontrol eder. Bir taslak,
yapılandırılan atıl süresini aştığında formbase bir teslimat gönderir.
Yerel API entegrasyonları webhooks.create’e idleWindow göndermelidir. Kabul edilen değerler 12h,
1d, 3d ve 1w’dir. Örtük bir varsayılan yoktur; değeri olmayan terk edilmiş bir abonelik reddedilir.
| Atıl süre | Açıklama |
|---|---|
| 12 saat | Aynı gün takipler için |
| 1 gün | Hatırlatmadan önce makul bir süre |
| 3 gün | Daha az acil formlar için |
| 1 hafta | Düşük frekanslı formlar için |
Payload yapısı, tamamlanmış bir submission ile aynıdır. İki fark şunlardır:
Yanıtlar seyrek olabilir — yalnızca katılımcının yanıtladığı sorular
answersvedisplayiçinde görünür.submittedAt— katılımcı hiçbir zaman resmi olarak göndermediği için gönderim zaman damgasına geri düşer.
Her entegrasyon, terk edilmiş bir taslak başına en fazla bir kez tetiklenir. Teslimat sonrası taslak, gelecekteki taramalardan hariç tutulur.
İmzalama
Her webhook’un kendi imzalama gizli anahtarı vardır — özel bir webhook yapılandırırken UI’de ayarlanır, ya da webhooks.create
üzerinde isteğe bağlı signingSecret parametresiyle (32–255 karakter). Bu,
istek geri çağırmaları için kullanılan çalışma alanı istek imzalama sırrı değildir, ama başlık ve
algoritma aynıdır, bu yüzden tek bir doğrulayıcı ikisini de karşılar.
İmzalanmış her teslimat X-formbase-Signature: t={seconds},sha256={hex} taşır. Özet,
{t}.{raw body} değerinin, gizli anahtarınızla anahtarlanmış bir HMAC-SHA256’sıdır. İki kural: herhangi
bir ayrıştırma veya yeniden serileştirmeden önce ham gövdeyi özetleyin ve sabit zamanda karşılaştırın.
import crypto from 'node:crypto'
export function verifyFormbaseWebhook(rawBody: string, header: string | undefined, secret: string, toleranceSeconds = 300): boolean {
if (!header) return false
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)))
if (!parts.t || !parts.sha256) return false
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(parts.sha256, 'hex')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false
return Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds
}import hashlib, hmac, time
def verify_formbase_webhook(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p)
t, received = parts.get('t'), parts.get('sha256')
if not t or not received:
return False
expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received):
return False
return abs(time.time() - int(t)) <= tolerance_secondsformbase bir tekrar oynatma (replay) penceresi zorunlu kılmaz, bu yüzden yukarıdaki tolerans sizin seçiminizdir. Gizli anahtarı değiştirmek, halen devam eden yeniden denemeler dahil, bir sonraki denemede geçerli olur — önce alıcınızı güncelleyin.
Üretimde her zaman doğrulayın
Doğrulama olmadan, URL’nizi bulan herkes sahte submission gönderebilir.
Yeniden denemeler
formbase, teslimat başına en fazla 5 deneme yapar — 1 ilk deneme ve 4 yeniden deneme — en az 1, 2, 4 ve 8 dakika aralıklarla. formbase,
süresi gelen yeniden denemeleri her 30 dakikada bir arar, bu yüzden bir yeniden deneme kendi geri çekilme aralığı bittikten yarım saat
sonrasına kadar gelebilir ve son deneme ilkinden yaklaşık iki saat sonra gelir. Yanıtınızdaki bir Retry-After başlığı, bir
sonraki geri çekilme adımından daha uzun bir süre istediğinde dikkate alınır. Uç noktanız şu durumlarda teslimat başarısız sayılır:
- 2xx dışında bir durum kodu döndürürse
- Zaman aşımına uğrarsa
- Bağlantıyı sıfırlarsa
Bir durum asla yeniden denenmez: engellenmiş, çözümlenemeyen veya özel bir adrese çözümlenen bir hedef. URL, DNS dahil, her denemeden hemen önce yeniden doğrulanır; bu yüzden izin verilmekten çıkan bir sunucu, bütçeyi tüketmek yerine teslimatı anında başarısız kılar.
5 ardışık başarısız teslimatın ardından entegrasyon duraklatılır. Uç noktayı düzeltip Form ayarları → Entegrasyonlar bölümünden yeniden etkinleştirin; başarılı bir teslimat sayacı sıfırlar.
Aynı olayın yeniden denenen teslimatları aynı id değerini kullanır, bu yüzden işlenen id’leri saklayarak yinelenenleri
ayıklayın. Gerçekten yeni bir olay — örneğin yanıtlayanın gönderimini düzenlemesi — yeni bir id ve
type: “submission.updated” ile gelir: Form ayarları webhook’unda ya da bir submission_updated aboneliğinde.
createdAt, denemenin yapıldığı an değil, olayın sıraya alındığı andır, bu yüzden yeniden denemeler arasında da aynı kalır.
Düzenlemeleri birbirinden ayırt etmek için data.submission.editCount’u okuyun: her düzenlemeyle birlikte artar;
data.submission.updatedAt ise sonuncusunun ne zaman gerçekleştiğini söyler.
Test
Entegrasyon kurulumu ve detay panelinde Test gönder düğmesi bulunur. Abone olunan etkinliğin bir örneğini URL’nize
gönderir, böylece gerçek bir submission veya istek beklemeden bağlantıyı doğrulayabilirsiniz. Aynı örnekler, API üzerinden
submissions.sample ve requests.sample olarak da kullanılabilir.
Yerel geliştirme için, geliştirme sunucunuzu bir tünel ile dışarıya açın:
# ngrok
ngrok http 3000
# cloudflare tunnel
cloudflared tunnel --url http://localhost:3000Yerel geliştirme
Tünel URL’sini webhook uç noktanız olarak kullanın, ardından uçtan uca doğrulamak için Test gönder’e tıklayın.