# Geri çağırmalar ve imzalama

Workflow’unuza devam edin ve çağrının gerçekten Formstep'ten geldiğini kanıtlayın.

## Geri çağırmalar ve imzalama

Bir istek sonuna ulaştığında — tamamlandığında, süresi dolduğunda veya iptal edildiğinde — Formstep, otomasyonunuzun verdiği URL'ye imzalı bir bildirim POST eder. Çalışmayı devam ettiren şey bu çağrıdır.

<h2 id="what-fires">Ne tetiklenir, ne zaman</h2>

<p>
  Üçü de aynı URL'ye gelir, bu yüzden yanıt olduğunu varsaymadan önce <code>type</code>’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.
</p>

<h2 id="payload">Ne gelir</h2>

```
{
  "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" }
  }
}
```

<ul>
  <li>
    <code>data.request</code> her zaman bulunur — <code>externalId</code>, <code>metadata</code> ve <code>context</code>’iniz dahil,
    değişmeden. Hangi sonlanma gerçekleştiyse onun zaman damgasını taşır (<code>completedAt</code>, <code>expiredAt</code>, ya da{' '}
    <code>canceledAt</code> ve isteğe bağlı bir <code>cancelReason</code> ile).
  </li>
  <li>
    <code>outcome</code> yalnızca alıcı bir <a href="/tr/requests/decisions-and-approvals">karar sorusunu</a> yanıtladığında bulunur —{' '}
    <code>approve</code>, <code>decline</code> ya da <code>changes</code>.
  </li>
  <li>
    <code>form</code>, <code>submission</code>, <code>answers</code> ve <code>display</code> yalnızca tamamlanmada görünür, tam olarak bir{' '}
    <a href="/tr/developers/webhooks-reference#payload">gönderim webhook’unun</a> taşıdığı şekilde. <code>form.snapshotId</code>, alıcının
    yanıtladığı tam yayınlanmış sürümdür; <code>submission.pdfUrl</code>, form bir gönderim PDF'i saklıyorsa bir URL'dir, aksi halde
    null'dur.
  </li>
  <li>
    <code>test</code>, istek <a href="/tr/requests/creating-requests#test-mode">test modunda</a> oluşturulduğunda <code>true</code> olur —
    buna göre dallanın ya da olayı yok sayın.
  </li>
  <li>
    <code>answers</code>, her örnek için iç içe yerleştirilmiş tekrarlayan gruplarla birlikte{' '}
    <a href="/tr/requests/field-keys">alan anahtarına</a> göre anahtarlanır. Bir seçim yanıtı, <code>fields.list</code>’ten gelen seçenek{' '}
    <strong>key</strong>’idir, etiketi değil; etiket aynı anahtar altında <code>display</code> içindedir.
  </li>
  <li>
    POST, <code>Content-Type: application/json</code> olarak ve <code>User-Agent: Formstep</code> ile gelir, ve{' '}
    <code>X-Formstep-Event-Id</code>, <code>X-Formstep-Event-Type</code> ve <code>X-Formstep-Signature</code> taşır — böylece ayrıştırmadan
    önce yinelenenleri ayıklayıp yönlendirebilirsiniz.
  </li>
</ul>

> ⚠️ **id üzerinde yinelenenleri ayıklayın**
> <p>
>     <code>id</code>, 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.
>   </p>

<h2 id="verify">İmzayı doğrulayın</h2>

<p>
  Her geri çağırma bir imza başlığı taşır, <code>X-Formstep-Signature: t=&#123;unix seconds&#125;,sha256=&#123;hex&#125;</code>. Hex, zaman
  damgasının, bir noktanın ve ham istek gövdesinin, çalışma alanınızın <strong>istek imzalama anahtarıyla</strong> hesaplanmış bir
  HMAC-SHA256'sıdır.
</p>

<p>Hangi dili kullanırsanız kullanın, iki kural:</p>

<ol>
  <li>
    Herhangi bir ayrıştırma veya yeniden serileştirmeden önce <strong>ham</strong> gövdeyi hashleyin. Yeniden kodlanmış JSON aynı baytlar
    değildir.
  </li>
  <li>
    Sabit zamanda karşılaştırın — <code>crypto.timingSafeEqual</code>, <code>hmac.compare_digest</code> — asla <code>==</code> ile değil.
  </li>
</ol>

  
    
```
import crypto from 'node:crypto'

  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_formstep_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
```

  

<h2 id="secret">İstek imzalama anahtarı</h2>

<p>
  Çalışma alanı başına tek bir anahtar, oradan gelen her geri çağırmayı imzalar. Bunu çalışma alanı kenar çubuğundaki{' '}
  <strong>OAuth ve API Anahtarları</strong>’nda, <strong>İstek imzalama anahtarı</strong> kartında bulun. Varsayılan olarak maskelenmiştir;{' '}
  <strong>Anahtarı göster</strong> 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 <code>callbackUrl</code> ile hiç istek
  oluşturmamış bir çalışma alanının henüz anahtarı yoktur.
</p>

> ❗ **Yeniden oluşturmanın bir hoşgörü süresi yoktur**
> <p>
>     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. <strong>Önce alıcınızı güncelleyin, sonra yeniden oluşturun.</strong> Her iki anahtarın da
>     kabul edildiği bir pencere yoktur.
>   </p>

<h2 id="retries">Yeniden denemeler</h2>

<p>
  Bir geri çağırma <strong>sekiz deneme</strong> alır: ilki, ardından en az 1, 2, 4, 8, 16, 32 ve 60 dakika aralıklı yedi yeniden deneme.
  Formstep, 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ı{' '}
  <code>id</code>’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.
</p>

<p>
  Bütçe tükenirse — alıcınız öğleden sonra boyunca kapalıysa — geri çağırma kaybolmaz. İstek bir <strong>Geri çağırma başarısız</strong>{' '}
  rozeti alır, çalışma alanı sahibine sunucu, neden ve deneme sayısıyla birlikte bir kez e-posta gönderilir ve yanıtlar{' '}
  <code>requests.get</code> üzerinden okunabilir kalır. Yeniden göndermek için isteği{' '}
  <a href="/tr/requests/managing-requests">İstekler sayfasında</a> açın ve <strong>Yeniden gönder</strong>’e basın, ya da{' '}
  <code>requests.replayCallback</code>’i çağırın. Aynı dondurulmuş yükü aynı <code>id</code> ile yeniden gönderir, bu da tam olarak
  yinelenenleri ayıklayan bir alıcının istediği şeydir.
</p>

<h2 id="subscriptions">Abonelikler aynı olayları duyar</h2>

<p>
  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 Formstep uygulamaları bunu sizin için yapar, ve <code>webhooks.create</code> bunu kodla, olay türü olarak{' '}
  <code>request_completed</code>, <code>request_expired</code> ya da <code>request_canceled</code> 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.{' '}
  <strong>Yeniden gönder</strong> yalnızca geri çağırmayı yeniden gönderir; bir abonelik kendi başına yeniden dener ve beş başarısız
  denemeden sonra duraklar.
</p>

> 💡 **Bir devam URL'si kimlik doğrulama değildir**
> <p>
>     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.
>   </p>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Sorun giderme](/tr/requests/troubleshooting) — Bir geri çağırma başarısız olmaya devam ettiğinde.
  - [Webhook referansı](/tr/developers/webhooks-reference) — Bir tamamlanmanın taşıdığı answers ve display eşlemeleri, eksiksiz.
</div>
