# Formlar

Forms modülü, satıcının client dashboard'da iletişim, teklif ve ön görüşme formları oluşturmasını ve yanıtları dashboard'daki gelen kutusunda almasını sağlar. Siteniz soruları koda gömmez: yayınlanmış formu **anahtarı** (key) ile yükler, render eder ve ziyaretçinin yanıtlarını geri gönderir. Borkol yanıtları sunucuda yeniden kontrol eder, başvuruyu saklar, numaralandırır, satıcıya ve ziyaretçiye e-posta gönderir ve bir webhook tetikler.

Dört endpoint'in tamamı `/v1/api/forms` altında yer alır, mağaza için `forms` modülünün etkin olmasını gerektirir (aksi halde `403 {"error":"Module not enabled: forms"}`) ve dakikada 120 isteklik mağaza geneli rate limit'e tabidir (bkz. [Hatalar ve Rate Limit'ler](/tr/api/getting-started/errors-and-rate-limits)).

| Endpoint | Scope | Amaç |
| --- | --- | --- |
| `GET /v1/api/forms/{key}` | `forms:read` | Etkin bir formun yayınlanmış sürümünü tek bir dilde yükleyin. Bkz. [Form yükleme](./forms). |
| `GET /v1/api/forms/address-lookup` | `forms:read` | Bir `address` alanı için posta kodu ve bina numarasından sokak ve şehri bulun. Bkz. [Adres arama](./forms#get-v1apiformsaddress-lookup). |
| `POST /v1/api/forms/{key}/uploads` | `forms:write` | Bir `file` alanı için tek bir dosya saklayın ve başvuru için bir token alın. Bkz. [Yüklemeler](./uploads). |
| `POST /v1/api/forms/{key}/submissions` | `forms:write` | Bir ziyaretçinin yanıtlarını gönderin. Bkz. [Başvurular](./submissions). |

## forms:write anahtarlarını sunucunuzda tutun

`forms:write` yetkisine sahip bir anahtar, satıcı adına dosya yükleyebilir ve başvuru gönderebilir; her başvuru e-posta gönderir. **Bir `forms:write` anahtarını asla tarayıcı koduna**, mobil uygulama paketine veya ziyaretçinin okuyabileceği başka bir yere koymayın. Formu yüklemeyi, dosya yüklemeyi ve gönderimi sunucunuzdan (bir route handler, server action veya backend endpoint'i ile) yapın; tarayıcının yalnızca kendi sunucunuzla konuşmasını sağlayın. Üç çağrının hepsini sunucunuz yaptığı için hem `forms:read` hem `forms:write` yetkisine sahip tek bir sunucu tarafı anahtar (örneğin `BORKOL_FORMS_KEY` ortam değişkeninde) yeterlidir. Formları yüklemek için yalnızca `forms:read` yetkili ayrı bir anahtar isteğe bağlıdır.

Bu durumda her istek sunucunuzun adresinden geldiği için, yükleme ve gönderim isteklerinde ziyaretçinin IP adresini `X-Borkol-Visitor-Ip` header'ında iletin. Borkol, saatlik sınırları ziyaretçi adresi başına uygular (bkz. [Ziyaretçi IP'si ve saatlik sınır](#ziyareti-ipsi-ve-saatlik-snr)).

## Akış

1. **Yükleyin**: sunucunuzda `GET /v1/api/forms/{key}?locale=nl`. Sonucu önbelleğe alın; [`form.published`](/tr/api/webhooks/index) webhook'u geldiğinde yenileyin.
2. **Render edin**: adımları sırayla gösterin. Bir adımı veya alanı yalnızca `show_if` kuralı sağlandığı sürece gösterin (bkz. [Kurallar](#kurallar)). İsterseniz yanıtları sayfa URL'sinden önceden doldurun (bkz. [URL'den önceden doldurma](#urlden-nceden-doldurma)).
3. **Kontrol edin**: ziyaretçi ilerlemeden önce her adımı sunucunun kullandığı kurallarla kontrol edin. `@borkol/forms` paketi bu kuralları uygular (bkz. [@borkol/forms paketi](#borkolforms-paketi)).
4. **Dosya yükleyin**: ziyaretçinin bir `file` alanı için seçtiği her dosyayı yükleyin; sunucunuzdan `POST /v1/api/forms/{key}/uploads` bir token döndürür (bkz. [Yüklemeler](./uploads)).
5. **Spam kontrolü**: form `settings.turnstile_site_key` döndürüyorsa Cloudflare Turnstile widget'ını gösterin ve token'ını gönderin (bkz. [Turnstile](#turnstile)).
6. **Gönderin**: yanıtlar, yükleme token'ları, Turnstile token'ı, ziyaretçinin dili, sayfa bilgileri ve ziyaretçi IP header'ı ile sunucunuzdan `POST /v1/api/forms/{key}/submissions`.
7. **Tamamlayın**: `201` yanıtında `thank_you.message` değerini gösterin veya ziyaretçiyi `thank_you.redirect_url` adresine gönderin. `422` yanıtında her hatayı kendi sorusunda gösterin ve Turnstile widget'ını sıfırlayın. `429` yanıtında ziyaretçiden daha sonra tekrar denemesini isteyin.

## Formlar, sürümler ve durum

Bir formun bir anahtarı, bir adı, bir durumu ve numaralı sürümleri vardır.

- **Anahtarı** satıcı seçer: harfle başlayan küçük harf, rakam ve kısa çizgi, en fazla 64 karakter (örneğin `quote-request`). Mağaza içinde benzersizdir ve form yayınlandıktan sonra değiştirilemez.
- Satıcı **Yayımlayın** düğmesine her tıkladığında mevcut taslak yeni ve dondurulmuş bir **sürüm** (1, 2, 3, ...) olur. Public API her zaman yalnızca en son yayınlanmış sürümü sunar; yayınlanmamış düzenlemeler sitenize asla ulaşmaz.
- Bir form `draft`, `active` veya `archived` durumundadır. Yalnızca en az bir kez yayınlanmış **etkin** bir form yüklenebilir ve gönderilebilir; diğer her durumda yanıt `404` olur. Yayınlamak formu etkinleştirmez; etkinleştirmek veya arşivlemek de hiçbir şey yayınlamaz.
- Her başvuru hangi sürüme karşı doldurulduğunu kaydeder; böylece sonraki sürümler soruları değiştirse bile satıcının gelen kutusu ziyaretçinin gördüğü soruları ve seçenek etiketlerini göstermeye devam eder.

## Tanım: adımlar ve alanlar

Formun gövdesi bir **adımlar** listesidir; her adımın bir **alanlar** listesi vardır. Bir formda en fazla 20 adım ve toplamda 150 girdi alanı (`heading` ve `paragraph` sayılmaz) olabilir. Adım ve alan anahtarları form içinde benzersizdir ve `/^[a-z][a-z0-9_]*$/` desenine uyar (en fazla 50 karakter). Yanıtlar alan anahtarı altında gönderilir ve saklanır.

Bir adım:

| Alan | Tür | Açıklama |
| --- | --- | --- |
| `key` | string | Adım anahtarı. |
| `title` | string | İstenen dildeki adım başlığı. |
| `show_if` | object veya null | Adımın gösterilip gösterilmeyeceğine karar veren kural; her zaman gösteriliyorsa `null`. |
| `fields` | array | Adımın alanları, sırasıyla. |

Bir alan:

| Alan | Tür | Açıklama |
| --- | --- | --- |
| `key` | string | Alan anahtarı; yanıt bu anahtar altında gönderilir. |
| `kind` | string | Aşağıdaki türlerden biri. |
| `label` | string | Soru. `heading` ve `paragraph` için gösterilecek metindir; `consent` için onay kutusunun yanındaki metindir; `hidden` için satıcının gelen kutusunda gördüğü addır (render etmeyin). |
| `help` | string veya null | Sorunun altındaki yardım metni. |
| `placeholder` | string veya null | Giriş alanının yer tutucusu. |
| `required` | boolean | Görünür bir alanın yanıtlanmasının zorunlu olup olmadığı. Eksik bir değeri `false` olarak kabul edin. |
| `show_if` | object veya null | Alanın kuralı; (görünür bir adım içinde) her zaman gösteriliyorsa `null`. |
| `config` | object | Türe özgü ayarlar; yoksa `{}`. |

Her metin zaten tek bir dile indirgenmiştir (bkz. [Diller](#diller)).

## Alan türleri

Giriş türleri bir yanıt toplar; `heading` ve `paragraph` yalnızca metin gösterir ve asla gönderilmez.

| Tür | `config` | Yanıt değeri |
| --- | --- | --- |
| `text` | `prefill?` | string, en fazla 255 karakter. |
| `textarea` | `prefill?` | string, en fazla 10000 karakter. |
| `number` | `prefill?` | JSON sayısı (tam sayı veya ondalık). `0` bir yanıt sayılır. |
| `email` | `prefill?` | string: geçerli bir e-posta adresi, en fazla 255 karakter. |
| `phone` | `prefill?` | string: rakam, boşluk ve `()-.` karakterlerinden oluşan, başında isteğe bağlı `+` olan 5 ile 30 karakter. |
| `date` | `prefill?` | string `YYYY-MM-DD`. |
| `select` | `options` (string listesi), `prefill?` | string: `options` değerlerinden biri. Seçenek metinleri her dilde yazıldığı gibi gösterilir. |
| `link` | `prefill?` | object `{url, label?}`: `url` en fazla 2000 karakterdir ve `https://`, `http://`, `/` (köke göreli), `mailto:` veya `tel:` ile başlar; `label` isteğe bağlıdır, en fazla 200 karakter. |
| `choice` | `options`, `multiple?`, `min?`, `max?`, `prefill?` | Bir seçenek `value` değeri (string) veya `multiple: true` ile seçenek değerlerinden oluşan bir liste (tekrarlar kaldırılır); ayarlanmışsa en az `min`, en fazla `max` öğe. |
| `consent` | `{}` | `true`. Zorunlu olması, kutunun işaretlenmesi gerektiği anlamına gelir. |
| `hidden` | `source?`: `query` (varsayılan) veya `site` | string, en fazla 500 karakter. Asla render edilmez; bkz. [Gizli alanlar](#gizli-alanlar). |
| `file` | `accept` (`image`, `pdf` veya ikisi), `max_files` (1 ile 10), `max_mb` (1 ile 20) | Yükleme token'larının listesi; `answers` içinde değil, `files` içinde gönderilir; aşağıdaki nota bakın. |
| `address` | `country` (ISO 3166-1 alpha-2, örneğin `NL`), `lookup?`, `service_area?` | object `{postal_code, house_number, addition?, street, city, lat?, lon?}`; aşağıdaki adres alanları notuna bakın. |
| `heading` | `{}` | Yok. `label` değerini başlık olarak gösterin. |
| `paragraph` | `{}` | Yok. `label` değerini metin olarak gösterin (en fazla 5000 karakter, satır sonları olan düz metin). |

Bir `choice` seçeneği `{value, label, description, icon}` şeklindedir: `value` gönderdiğiniz değerdir (en fazla 100 karakter), `label` seçenek metnidir, `description` isteğe bağlı ikinci satırdır (string veya null), `icon` isteğe bağlı bir simge adıdır (`/^[a-z][a-z0-9-]{0,39}$/`) veya `null`. Bir `choice` alanında 1 ile 30 seçenek bulunur; bir `select` alanında en fazla 100 karakterlik 1 ile 50 seçenek metni bulunur.

**Bir koleksiyondan seçenekler.** Bir `choice` alanı seçeneklerini sabit bir liste yerine bir nesne koleksiyonundan alabilir. Bu durumda `config`, elle yazılmış bir `options` listesi yerine `options_source` taşır:

```json title="Config"
{
  "multiple": false,
  "options_source": {
    "object_type": "services",
    "label": "title",
    "description": "summary",
    "sort": "position",
    "limit": 30
  }
}
```

| Ad | Tür | Zorunlu | Açıklama |
| --- | --- | --- | --- |
| `object_type` | string | Evet | Mağazanın bir nesne türünün anahtarı. |
| `label` | string | Evet | Seçenek etiketi olarak kullanılan bir metin özelliğinin anahtarı. |
| `description` | string veya null | Hayır | Seçenek açıklaması olarak kullanılan bir metin özelliğinin anahtarı. |
| `sort` | string veya null | Hayır | Bir sayı özelliğinin anahtarı. Seçenekler buna göre artan sırada dizilir, değeri olmayan kayıtlar sonda; `sort` yoksa oluşturulma sırasını izler. |
| `limit` | integer | Hayır | Kaç seçenek sunulacağı, 1 ile 100. Varsayılan 30. |

`GET /v1/api/forms/{key}` yanıtında sunucu seçenekleri zaten doldurmuştur: `config.options` güncel seçenekleri her zamanki `{value, label, description, icon}` biçiminde taşır (`icon` `null` olur) ve `config.options_source` nesnenin içinde kalır. `options` değerini render edin, `options_source` değerini yok sayın. Seçenekler, türün herkese açık görünen kayıtlarıdır (taslaklar ve zamanlanmış kayıtlar dışarıda kalır). `value`, türün adresleri varsa kaydın mağazanın varsayılan dilindeki adresi (slug), yoksa kayıt kimliğidir; varsayılan dilde adresi olmayan kayıt dışarıda bırakılır. `label` ve `description` istenen dilde okunur, yoksa varsayılan dile düşülür. Her istekte yeniden aranırlar, yani seçenekler koleksiyonu izler: önbelleğe alınmış bir formu bir [`object_record.published`, `object_record.updated`, `object_record.unpublished` veya `object_record.deleted`](/tr/api/webhooks/index) webhook'u geldiğinde yenileyin. Bir başvuru o andaki seçeneklere göre kontrol edilir; artık seçenek olmayan bir yanıt `Choose one of the options.` hatasını verir. `multiple: true` ile `min` ve `max` en fazla `limit` olabilir. Bir alanda `options` veya `options_source` bulunur, ikisi birden asla.

Bir `consent` etiketi markdown ile yazılmış tek bir bağlantı içerebilir, `[gizlilik politikası](https://example.com/privacy)`, her zaman `https://`. Bunu bağlantı olarak render edin.

**Dosya alanları.** Bir `file` alanı iki adımda yanıtlanır. Önce her dosyayı `POST /v1/api/forms/{key}/uploads` (multipart `field` ve `file`) ile yükleyin; bu endpoint türü dosyanın içeriğine göre, boyutu `max_mb` değerine göre kontrol eder ve bir token döndürür. Ardından token'ları başvurunun `files` nesnesinde, alan anahtarından token listesine (en fazla `max_files`) eşleyerek gönderin, asla `answers` içinde değil: bir dosya alanı için `answers` içinde gönderilen değer yok sayılır. Bir token 24 saat geçerlidir, tek bir forma ve tek bir alana aittir ve yalnızca bir başvuru için çalışır. Bkz. [Yüklemeler](./uploads).

**Adres alanları.** Bir `address` alanı, `answers` içinde tek bir nesneyle yanıtlanır. `postal_code`, `house_number`, `street` ve `city` zorunludur (eksik bir parça `This field is required.` hatasını verir); `addition` isteğe bağlıdır; `lat` ve `lon` isteğe bağlı sayılardır (enlem -90 ile 90, boylam -180 ile 180). Baştaki ve sondaki boşluklar kırpılır ve boş bir parça eksik sayılır. Sunucu `country` değerini `config.country` içinden ekler. Hollanda (`NL`) posta kodu dört rakam ve iki harftir, boşluklu, tireli veya bitişik olabilir ve `1234 AB` olarak saklanır; diğer ülkelerde posta kodu 2 ile 12 karakterdir. Kırpmadan sonra `postal_code` en fazla 12, `house_number` en fazla 20, `addition` en fazla 10, `street` ve `city` ise en fazla 255'er karakterdir. Değerdeki diğer her sorun (nesne olmaması, geçersiz posta kodu, fazla uzun bir parça, aralık dışı koordinatlar) `Must be a valid address.` hatasını verir. Bir kural bir adresi yalnızca `filled` veya `empty` ile test edebilir.

`config.lookup: true` ile formunuz, posta kodu ve bina numarasından `street`, `city`, `lat` ve `lon` değerlerini [`GET /v1/api/forms/address-lookup`](./forms#get-v1apiformsaddress-lookup) ile doldurabilir. Arama yalnızca `NL` için vardır; satıcı başka bir ülke için açamaz. Ziyaretçi sokak ve şehri kendisi de yazabilir, örneğin arama hiçbir şey bulmazsa.

`config.service_area` şu biçimdedir: `{origin: {lat, lon, label?}, radius_km, mode, message}`. `radius_km` 0'dan büyük ve en fazla 1000'dir, `mode` `notice` veya `block` olur, `message` ise bölge dışındaki bir adres için gösterilecek metindir. Sunucu, `origin` noktasından adres koordinatlarına kuş uçuşu mesafeyi kilometre olarak (tek ondalık) ölçer ve başvurunun `meta.address` alanında her adres alanı için `{distance_km, in_area}` saklar; satıcı bunu gelen kutusunda görür. Koordinatların nasıl belirlendiği ve `block` modunun bir başvuruyu ne zaman reddettiği [Adres alanları ve hizmet bölgesi](./submissions#adres-alanlar-ve-hizmet-blgesi) bölümünde anlatılır.

### Neyin boş sayıldığı

Bir yanıt `null`, yalnızca boşluk, sekme veya satır sonlarından oluşan bir string, boş liste `[]`, boş nesne `{}` veya `false` ise boştur. `0` dahil diğer her şey bir yanıttır. Zorunlu ve görünür bir alana verilen boş yanıt bir hatadır; isteğe bağlı bir alana verilen boş yanıt ise yalnızca saklanmaz.

## Kurallar

Bir adım veya alandaki `show_if`, onun gösterilip gösterilmeyeceğine karar verir. Tek bir koşul veya tek bir koşul grubudur:

```json title="Rules"
{ "field": "services", "op": "contains", "value": "website" }

{ "all": [
  { "field": "services", "op": "contains", "value": "website" },
  { "field": "budget", "op": "not_equals", "value": "small" }
] }

{ "any": [
  { "field": "job_type", "op": "equals", "value": "roof" },
  { "field": "job_type", "op": "equals", "value": "paint" }
] }
```

`all`, her koşul sağlandığında; `any`, en az biri sağlandığında geçer. Gruplar iç içe olamaz. Bir koşul, formda **daha önce** gelen bir giriş alanını belirtir: bir alanın kuralı için kendisinden önceki bir soru (önceki bir adımda veya aynı adımda daha yukarıda); bir adımın kuralı için önceki bir adımdaki bir soru.

| `op` | Şu durumda geçer |
| --- | --- |
| `equals` | Liste yanıtının tam olarak bir öğesi vardır ve bu öğe `value` değerine eşittir; diğer yanıtlar `value` değerine eşittir. Boş bir yanıt hiçbir zaman eşit değildir. |
| `not_equals` | `equals` geçmez (yani boş bir yanıt geçer). |
| `contains` | Liste yanıtında `value` değerine eşit bir öğe vardır; diğer yanıtlar `value` değerine eşittir. |
| `filled` | Yanıt boş değildir. `value` yok sayılır. |
| `empty` | Yanıt boştur. `value` yok sayılır. |

`equals`, `not_equals` ve `contains` için `value` bir string, sayı veya boolean'dır. Değerler metin olarak karşılaştırılır: string'ler olduğu gibi, `true` ve `false` sırasıyla `"true"` ve `"false"` olarak, tam sayılar ondalıksız (`2`, `2.0` ve `"2"` eşittir), diğer sayılar en kısa biçimleriyle (`2.5`). `true`, `"true"` değerine eşittir ama `1` değerine eşit değildir; nesneler ve `null` hiçbir şeye eşit değildir.

Formu sırayla dolaşın. Bir adım, kuralı geçtiğinde gösterilir; bir alan, adımı gösterildiğinde ve kendi kuralı geçtiğinde gösterilir. Bir koşul, bir alanın yanıtını yalnızca o alan gösterildiği sürece okur: gizli bir alanın yanıtı boş okunur; bu yüzden bir alanı gizlemek ona bağlı her şeyi de gizler. Sunucu gönderim sırasında tam olarak bu kuralları uygular: o anda gizli olan alanlara verilen yanıtlar hata vermeden atılır ve zorunlu değildir. Gizli bir dosya alanı için gönderilen yükleme token'ları da atılır ve talep edilmez.

## Gizli alanlar

Bir `hidden` alanı asla gösterilmez, ancak değeri başvuruyla birlikte saklanır (örneğin bir kampanya kodu veya ziyaretçinin geldiği sayfa).

- `source: "query"` (varsayılan): değeri, sayfa URL'sindeki alan anahtarıyla aynı adı taşıyan sorgu parametresinden alın; örneğin `campaign` anahtarlı bir alan için `?campaign=spring`.
- `source: "site"`: değeri form gösterilirken siteniz kendisi belirler.

Değer en fazla 500 karakterlik bir string'dir. Görünürlük kuralları gizli alanlar için de geçerlidir.

## URL'den önceden doldurma

`config.prefill` değeri `true` olan bir alan, sayfa URL'sindeki alan anahtarıyla aynı adı taşıyan sorgu parametresinden doldurulabilir (`?email=ada@example.com`). Ziyaretçi yine de değiştirebilir; sunucu bunu normal bir yanıt olarak ele alır. Onay ve dosya alanları asla önceden doldurulmaz.

## Diller

Form metinleri dashboard'da dil başına yazılır. `GET /v1/api/forms/{key}?locale=` her metni o dilde döndürür; yoksa mağazanın varsayılan diline, o da yoksa metni olan herhangi bir dile geri döner. Mağaza için etkin olmayan bir `locale` varsayılan dili sunar (hata vermez). Başvuruda da aynı `locale` değerini gönderin: yanıttaki teşekkür mesajının ve ziyaretçiye giden onay e-postasının dilini belirler.

## Ziyaretçi IP'si ve saatlik sınır

Her formun, satıcının belirlediği ziyaretçi başına saatlik bir başvuru sınırı vardır (1 ile 100 arası, varsayılan 5). `422` ile doğrulamadan geçemeyenler dahil her deneme sayılır; sınırın üzerinde API `429 {"message": "Too many submissions. Try again later."}` döndürür. Saat, ziyaretçinin ilk denemesiyle başlar.

Borkol ziyaretçiyi, sunucunuzun ziyaretçinin IP adresiyle (IPv4 veya IPv6) doldurduğu `X-Borkol-Visitor-Ip` header'ı ile tanır. Yalnızca kendi proxy'nizin veya barındırma sağlayıcınızın belirlediği bir adrese güvenin: güvenilir bir proxy üzerine yazmadıkça `X-Forwarded-For` header'ının ilk adımı ziyaretçi tarafından taklit edilebilir. Header yoksa veya geçerli bir IP adresi değilse Borkol bağlantının adresini, yani sunucunuzu kullanır; bu durumda tüm ziyaretçileriniz tek bir sınırı paylaşır. Header yalnızca form route'larında okunur.

Ham IP adresi asla saklanmaz: başvuru yalnızca onun HMAC-SHA256 hash'ini tutar.

Yüklemelerin kendi sınırı vardır: form ve ziyaretçi başına saatte 30, aynı şekilde sayılır (bkz. [Yüklemeler](./uploads)).

## Honeypot

İnsanların hiç görmediği bir giriş alanı ekleyin (`type="hidden"` ile değil, CSS ile gizlenmiş; `tabindex="-1"` ve `autocomplete="off"` ile) ve değerini `honeypot` olarak gönderin. Botlar genellikle her alanı doldurur. `honeypot` boş bir string veya `null` dışında herhangi bir değer taşıyorsa Borkol `201` ile `number: null` ve formun teşekkür ayarlarını döndürür; hiçbir şey saklamaz, e-posta veya webhook göndermez, Turnstile'ı kontrol etmez ve denemeyi saatlik sınıra saymaz.

## Turnstile

Satıcı [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) özelliğini form başına açabilir ve mağazanın Turnstile site key ile secret key değerlerini dashboard'da bir kez kaydeder. Formda Turnstile açıksa ve iki anahtar da ayarlanmışsa `GET /v1/api/forms/{key}` `settings.turnstile_site_key` döndürür; aksi halde bu alan yoktur.

- Turnstile widget'ını bu site key ile render edin ve ürettiği token'ı başvuruyla birlikte `turnstile_token` olarak gönderin.
- Borkol token'ı saatlik sınırdan sonra ve yanıtları kontrol etmeden önce Cloudflare'de doğrular (`X-Borkol-Visitor-Ip` içindeki ziyaretçi IP'si ile). Eksik, geçersiz, süresi dolmuş veya daha önce kullanılmış bir token, bir Cloudflare hatası ya da 5 saniyeden uzun süren bir kontrol, hatası `turnstile` üzerinde olan bir `422` döndürür: `The spam check failed. Reload the page and try again.`
- **Bir token yalnızca bir kez çalışır.** Token yanıtlardan önce kontrol edildiği için her `422` (yanlış bir yanıt da) token'ı tüketmiştir. Her `422` sonrasında widget'ı sıfırlayın ve yeni token'ı sonraki denemeyle gönderin.
- Başarısız bir kontrol de saatlik sınıra sayılır.

Formda Turnstile açık olduğu halde mağaza iki anahtarı da ayarlamamışsa hiçbir şey doğrulanmaz: `turnstile_site_key` yoktur, bir `turnstile_token` yok sayılır ve başvurular kontrol olmadan kabul edilir. Dashboard satıcıyı bu konuda formda ve Forms ayarları sayfasında uyarır.

## Başvurudan sonra ne olur

Borkol görünür yanıtları (`hidden` alan değerleri dahil) form sürümü, dil ve gönderdiğiniz sayfa bilgileriyle birlikte saklar. Ziyaretçinin `name` değerini `name` anahtarlı alandan, `email` değerini yanıtlanmış ilk `email` alanından, `phone` değerini yanıtlanmış ilk `phone` alanından alır. Yüklenen dosyalar başvuruya eklenir ve gizli olarak saklanır: satıcı bunları gelen kutusundan açar; herkese açık bir URL yoktur. Bu e-posta adresi mağazanın tam olarak bir kişisiyle (büyük/küçük harf duyarsız) eşleşirse başvuru o kişiye bağlanır; Borkol asla kişi oluşturmaz.

Başvuru saklandıktan sonra Borkol satıcıya e-posta gönderir, form bunu içeriyorsa ve ziyaretçi bir e-posta adresi verdiyse ziyaretçiye onay e-postası gönderir, dashboard'a bir bildirim ekler ve [`form_submission.created`](/tr/api/webhooks/index) webhook'unu gönderir. Bunların hiçbiri isteği başarısız kılamaz: saklandıktan sonra yanıt `201` olur.

Public API'de başvuruları geri okumak için bir endpoint yoktur. Satıcı bunları dashboard'daki gelen kutusunda ele alır.

## @borkol/forms paketi

`@borkol/forms`, bu endpoint'lerin türlerini ve sunucuyla aynı kuralları içeren, bağımlılığı olmayan bir TypeScript paketidir (MIT); böylece siteniz göndermeden önce aynı adımları ve aynı hataları gösterebilir. `@borkol/forms` paketini kurun; React isteğe bağlı bir peer'dır ve yalnızca `@borkol/forms/react` tarafından kullanılır.

```ts title="Rules"
import { visible, check, prefill, type PublicForm } from "@borkol/forms";

const form: PublicForm = (await res.json()).data;

visible(form, answers);          // { steps: ["what", ...], fields: ["services", ...] }
check(form, answers);            // { errors: { "answers.email": ["Must be an email address."] }, clean: {...} }
check(form, answers, "what");    // check one step only
prefill(form, new URLSearchParams(location.search));
```

- `visible`, gösterilen adımların ve alanların anahtarlarını döndürür.
- `check`, `answers.{key}` anahtarlı ve sunucunun İngilizce mesajlarını içeren `errors` değerini (kendi çevirilerinize eşleyebilmeniz için `MESSAGES` olarak dışa aktarılır) ve gönderilecek yanıtlar olan `clean` değerini döndürür: kurallarıyla gizlenen alanlar, görüntüleme türleri ve bilinmeyen anahtarlar çıkarılmıştır. `hidden` türündeki değerler korunur.
- `prefill`, `config.prefill` içeren alanların ve `query` kaynaklı `hidden` alanların yanıtlarını URL sorgu parametrelerinden okur.

`@borkol/forms/react`, adımları sizin bileşenlerinizle render eder:

```tsx title="React"
import { BorkolForm } from "@borkol/forms/react";

<BorkolForm
  form={form}
  locale="nl"
  onSubmit={(body) => submitToMyServer(body)} // resolves to { number, thank_you }
  components={{
    text: ({ field, value, onChange, errors }) => (
      <label>{field.label}<input value={(value as string) ?? ""} onChange={(e) => onChange(e.target.value)} />{errors[0]}</label>
    ),
    heading: ({ field }) => <h2>{field.label}</h2>,
    nav: ({ isFirst, isLast, back, submitting }) => (
      <>{!isFirst && <button type="button" onClick={back}>Back</button>}<button disabled={submitting}>{isLast ? "Send" : "Next"}</button></>
    ),
    done: ({ result }) => <p>{result.thank_you.message}</p>,
  }}
/>;
```

`onSubmit`, kontrol edilmiş yanıtlarla birlikte `{locale, answers, files?, turnstile_token?}` başvuru gövdesini alır; bunu kendi sunucunuza gönderin; sunucunuz `meta` ve `honeypot` ekleyebilir ve gövdeyi `forms:write` anahtarı ve ziyaretçi IP header'ı ile iletir. Sunucunun `422` hatalarını göstermek için yanıt gövdesini (`errors` içeren nesneyi) `onSubmit` içinden fırlatın: gösterilen sorulardaki hatalar o sorularda görünür, diğer her anahtar (`turnstile`, şu anda gizli olan bir soru) `formErrors` içine gider; bunu `nav` bileşeni ve `formError` bileşeni alır. Fırlatılan başka her şey (bir ağ hatası) orada genel bir mesaj gösterir; `submit()` asla reddedilmez.

Diğer seçenekler:

- `upload(field, file)` bir `file` alanı için tek bir dosya saklar: dosyayı multipart (`field`, `file`) olarak sunucunuzdaki ve onu `POST /v1/api/forms/{key}/uploads` adresine ileten bir route'a gönderin ve yanıtın `data` değeriyle (`{token, name, size}`) resolve edin. Mesajı sorunun altında göstermek için `422` gövdesini fırlatın. Bir dosya alanının yanıtı bu durumda yüklenen dosyaların listesidir; `file` bileşeni `upload`, `remove`, `uploading` ve `accept` (giriş alanının `accept` özniteliği için) değerlerini alır. Paket yüklemeden önce `max_files` ve `max_mb` değerlerini kontrol eder ve token'ları `files` altında gönderir.
- `turnstileToken`, Turnstile widget'ının güncel token'ıdır ve `turnstile_token` olarak gönderilir.
- `onServerErrors(errors)` her başarısız gönderimden sonra çağrılır. Turnstile widget'ını burada sıfırlayın: token'ı tükenmiştir.
- `persistKey`, ziyaretçinin taslağını (yanıtlar ve geçerli adım) `localStorage[persistKey]` içinde tutar ve mount'tan hemen sonra geri yükler; böylece sayfa yenilense de ziyaretçi kaldığı yerden devam eder. `initialAnswers` ayarladığı anahtarlarda önceliklidir, dosya yanıtları saklanmaz ve taslak başarılı bir gönderimden sonra silinir. 30 günden eski bir taslak yok sayılır ve silinir. `localStorage` yoksa form eskisi gibi çalışır.
- Bir `address` alanı her zamanki `components.address` prop'larını alır (`field`, `value`, `onChange`, `errors`); arama sizin bileşeninizde kalır. `formatAddress`, `normaliseAddress` ve `distanceKm` paketin kökünden dışa aktarılır.

`useBorkolForm({ form, initialAnswers, persistKey, locale, onSubmit, upload, turnstileToken, onServerErrors })`, tamamen özel bir yerleşim için aynı durumu verir: `step`, `stepIndex`, `steps`, `fields`, `answers`, `errors` (alan anahtarına göre), `formErrors`, `setAnswer`, `upload(key, files)`, `removeFile(key, token)`, `uploading`, `next()` (mevcut adımı kontrol eder), `back()`, `submit()`, `submitting` ve `result` (honeypot isabetinden sonra `result.number` `null` olur). `site` kaynaklı `hidden` alanları `initialAnswers` içinde iletin.

Sunucu belirleyici olmaya devam eder: site ne yaparsa yapsın her başvuruyu yeniden kontrol eder.
