formbasedocs
Uygulamaya gitUygulama

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/json
  • X-formbase-Signature: t={timestamp},sha256={hex} — imzalama gizli anahtarı ayarlandığında eklenir (aşağıya bakın)

  • X-formbase-Event-Id ve X-formbase-Event-Type — gövdedeki id ve type ile 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-Type hariç. webhooks.create ile 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:

EtkinlikNe zaman tetiklenir
submission_createdBir katılımcı formu tamamlayıp gönderdiğinde. Bu varsayılan seçenektir.
submission_updatedForm gönderim sonrası düzenlemeye izin veriyorsa, bir katılımcı daha önce gönderdiği bir gönderimi düzenlediğinde.
submission_abandonedBir taslak submission, yapılandırılan süre boyunca atıl kaldığında. Kısmi submission takibi gerektirir (Pro).
request_completedBir alıcı formdaki bir isteği tamamlar. İstek bloğunu ve yanıtları taşır.
request_expiredFormdaki bir isteğin süresi, alıcı tamamlamadan dolar. Yalnızca istek bloğu.
request_canceledFormdaki 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 submission

  • submission.updated — mevcut bir submission düzenlendi

  • submission.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ı

POST body
json
{
  "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"
    }
  }
}
AnahtarNe olduğu
idEtkinlik kimliği. Yeniden denemeler aynısını kullanır — yinelenenleri bunun üzerinden ayıklayın.
typeYukarıdaki altı etkinlik türünden biri.
createdAtEtkinliğin sıraya alındığı an, bu teslimat denemesinin yapıldığı an değil. Yeniden denemeler arasında sabit kalır.
apiVersionPayload 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.
testBir örnek veya test teslimatı için, ve test modunda oluşturulan bir istek için true. Her zaman bulunur.
data.form.snapshotIdKatı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.updatedAtKatılımcının gönderimi en son ne zaman düzenlediği; ilk düzenlemeye kadar null.
data.submission.editCountKatılımcının gönderimi gönderdikten sonra kaç kez düzenlediği: submission.completed üzerinde 0, ilk düzenlemede 1.
data.answersHer yanıt, alan anahtarıyla anahtarlanmış. Her yanıt yalnızca bir kez görünür.
data.displayAynı 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:

data.answers
json
{
  "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:

data.schema
json
[
  { "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.

data'ya eklenir
json
"request": {
  "id": "kd7...",
  "externalId": "run-42",
  "metadata": { "crm_record": "rec_88" }
}
  • id — bu gönderinin yanıtladığı istek. Tam görünüm için requests.get’e geçirin.

  • externalId ve metadata — 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üreAçıklama
12 saatAynı gün takipler için
1 günHatırlatmadan önce makul bir süre
3 günDaha az acil formlar için
1 haftaDüşü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 answers ve display iç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.

verify.ts
ts
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
}
verify.py
python
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_seconds

formbase 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.

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:

bash
# ngrok
ngrok http 3000

# cloudflare tunnel

cloudflared tunnel --url http://localhost:3000

Yerel 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.

Sonraki adımlar