İstekler
Geri çağırmalar ve imzalama
Bir istek sonuna ulaştığında — tamamlandığında, süresi dolduğunda veya iptal edildiğinde — formbase, otomasyonunuzun verdiği URL'ye imzalı bir bildirim POST eder. Çalışmayı devam ettiren şey bu çağrıdır.
Ne tetiklenir, ne zaman
| Olay | Ne zaman |
|---|---|
| request.completed | Alıcı gönderdi. Yanıtları taşır. |
| request.expired | İstek beklemedeyken süre dolumu geçti. |
| request.canceled | Siz veya otomasyonunuz geri çekti. |
Üçü de aynı URL’ye gelir, bu yüzden yanıt olduğunu varsaymadan önce type’a göre dallanın. Her sonlanmada tetiklenmenin tüm
amacı budur: bir müşteride park halinde duran bir workflow, ister yanıtlasın, ister sizi görmezden gelsin, ister iptal edilsin devam eder.
Ne gelir
{
"id": "evt_kj7...",
"type": "request.completed",
"createdAt": "2026-03-04T09:31:40.000Z",
"apiVersion": "2026-09-24",
"test": false,
"data": {
"request": {
"id": "kd7...",
"externalId": "run-42",
"status": "completed",
"outcome": "approve",
"language": "en",
"recipient": { "email": "ada@acme.com", "name": "Ada" },
"metadata": { "runId": "run-42" },
"context": { "case_id": "CASE-9" },
"createdAt": "2026-03-04T09:20:00.000Z",
"completedAt": "2026-03-04T09:31:40.000Z"
},
"form": { "id": "j57...", "name": "Vendor onboarding", "snapshotId": "kx2..." },
"submission": {
"id": "jd7...",
"respondentEmail": "ada@acme.com",
"submittedAt": "2026-03-04T09:31:40.000Z",
"updatedAt": null,
"editCount": 0,
"pdfUrl": null,
"language": "en"
},
"answers": { "company_name": "Acme", "contacts": [{ "name": "Ada" }] },
"display": { "company_name": "Acme", "contacts": "Ada" }
}
}data.requesther zaman bulunur —externalId,metadatavecontext’iniz dahil, değişmeden. Hangi sonlanma gerçekleştiyse onun zaman damgasını taşır (completedAt,expiredAt, ya dacanceledAtve isteğe bağlı bircancelReasonile).outcomeyalnızca alıcı bir karar sorusunu yanıtladığında bulunur —approve,declineya dachanges.form,submission,answersvedisplayyalnızca tamamlanmada görünür, tam olarak bir gönderim webhook’unun taşıdığı şekilde.form.snapshotId, alıcının yanıtladığı tam yayınlanmış sürümdür;submission.pdfUrl, form bir gönderim PDF’i saklıyorsa bir URL’dir, aksi halde null’dur.test, istek test modunda oluşturulduğundatrueolur — buna göre dallanın ya da olayı yok sayın.answers, her örnek için iç içe yerleştirilmiş tekrarlayan gruplarla birlikte alan anahtarına göre anahtarlanır. Bir seçim yanıtı,fields.list’ten gelen seçenek key’idir, etiketi değil; etiket aynı anahtar altındadisplayiçindedir.POST,
Content-Type: application/jsonolarak veUser-Agent: formbaseile gelir, veX-formbase-Event-Id,X-formbase-Event-TypeveX-formbase-Signaturetaşır — böylece ayrıştırmadan önce yinelenenleri ayıklayıp yönlendirebilirsiniz.
id üzerinde yinelenenleri ayıklayın
id, aynı olayın her yeniden denemesinde ve her tekrar oynatmasında sabit kalır. Alıcınız aynı kimlikte iki kez harekete
geçebilirse — çift bir fatura, çift bir bilet — zaten işlediğiniz kimlikleri hatırlayın.
İmzayı doğrulayın
Her geri çağırma bir imza başlığı taşır, X-formbase-Signature: t={unix seconds},sha256={hex}. Hex, zaman
damgasının, bir noktanın ve ham istek gövdesinin, çalışma alanınızın istek imzalama anahtarıyla hesaplanmış bir
HMAC-SHA256’sıdır.
Hangi dili kullanırsanız kullanın, iki kural:
Herhangi bir ayrıştırma veya yeniden serileştirmeden önce ham gövdeyi hashleyin. Yeniden kodlanmış JSON aynı baytlar değildir.
Sabit zamanda karşılaştırın —
crypto.timingSafeEqual,hmac.compare_digest— asla==ile değil.
import crypto from 'node:crypto'
export function verifyFormbaseCallback(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
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_callback(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İstek imzalama anahtarı
Çalışma alanı başına tek bir anahtar, oradan gelen her geri çağırmayı imzalar. Bunu çalışma alanı kenar çubuğundaki
OAuth ve API Anahtarları’nda, İstek imzalama anahtarı kartında bulun. Varsayılan olarak maskelenmiştir;
Anahtarı göster onu gösterir ve kopyala düğmesi kopyalar. Tek seferlik bir değer değildir — geri dönüp tekrar
okuyabilirsiniz. Anahtar, ilk ihtiyaç duyulduğu anda üretilir, bu yüzden o kartı hiç açmamış ve bir callbackUrl ile hiç istek
oluşturmamış bir çalışma alanının henüz anahtarı yoktur.
Yeniden oluşturmanın bir hoşgörü süresi yoktur
Yalnızca çalışma alanı sahibi anahtarı yeniden oluşturabilir ve bunu yaptığı an eskisi çalışmayı durdurur — halihazırda yeniden denenmekte olan geri çağırmalar da dahil. Önce alıcınızı güncelleyin, sonra yeniden oluşturun. Her iki anahtarın da kabul edildiği bir pencere yoktur.
Yeniden denemeler
Bir geri çağırma sekiz deneme alır: ilki, ardından en az 1, 2, 4, 8, 16, 32 ve 60 dakika aralıklı yedi yeniden deneme.
formbase, süresi gelen yeniden denemeleri her 30 dakikada bir arar, bu yüzden bir yeniden deneme kendi aralığı bittikten yarım saat
sonrasına kadar gelebilir ve son deneme istek sona erdikten yaklaşık dört saat sonra gelir. Her deneme aynı baytları ve aynı
id’yi taşır: yük, isteğin sona erdiği anda dondurulur, bu yüzden bir yeniden deneme o anda ne olduğunu anlatır, isteğin şu an
nasıl göründüğünü değil. Hedef URL ve imzalama anahtarı her denemede yeniden okunur, yükle birlikte dondurulmaz.
| Yanıtınız | formbase ne yapar |
|---|---|
| 2xx | Tamamlandı. Geri çağırma teslim edildi olarak işaretlenir. |
| 408, 429, 5xx | Retry-After gönderdiğinizde ona uyarak yeniden dener. |
| Diğer 4xx | Durur. Uç noktanız çağrıyı reddetti, aynı gövdeyi yeniden denemek yardımcı olamaz. |
| Zaman aşımı veya bağlantı hatası | Aynı programda yeniden dener. |
| Engellenen URL | Hemen durur. Çözümlenmeyen bir sunucu, özel bir adres ya da HTTPS olmayan bir URL asla izin verilen hale gelemez. Yönlendirmeler asla izlenmez, bu yüzden bir 3xx de durur. |
Bütçe tükenirse — alıcınız öğleden sonra boyunca kapalıysa — geri çağırma kaybolmaz. İstek bir Geri çağırma başarısız
rozeti alır, çalışma alanı sahibine sunucu, neden ve deneme sayısıyla birlikte bir kez e-posta gönderilir ve yanıtlar
requests.get üzerinden okunabilir kalır. Yeniden göndermek için isteği
İstekler sayfasında açın ve Yeniden gönder’e basın, ya da
requests.replayCallback’i çağırın. Aynı dondurulmuş yükü aynı id ile yeniden gönderir, bu da tam olarak
yinelenenleri ayıklayan bir alıcının istediği şeydir.
Abonelikler aynı olayları duyar
Bir geri çağırma URL’si tek bir isteğe aittir. Bir formdaki her istek aynı alıcıya ulaşmalıysa, bunun yerine bir kez abone olun: Zapier ve
n8n için formbase uygulamaları bunu sizin için yapar, ve webhooks.create bunu kodla, olay türü olarak
request_completed, request_expired ya da request_canceled ile yapar. Bir abonelik bu aynı zarfı
alır; çalışma alanının istek imzalama anahtarı yerine kendi imzalama sırrıyla imzalanmış, kendi olay kimliği ve kendi yeniden deneme
bütçesiyle. Hem bir geri çağırma URL’si hem de eşleşen bir aboneliği olan bir istek iki kez tetiklenir, her birine bir kez.
Yeniden gönder yalnızca geri çağırmayı yeniden gönderir; bir abonelik kendi başına yeniden dener ve beş başarısız
denemeden sonra duraklar.
Bir devam URL'si kimlik doğrulama değildir
Workflow araçları size tahmin edilmesi zor bir devam URL’si verir ve bunu bir kanıt olarak ele almak cazip gelir. Bu bir taşıyıcı gizli değeridir — günlüklere sızabilir ve gövdenin kurcalanmadığını size söylemez. İmzayı devam eden dalda da doğrulayın.