Sorgu sözleşmesi

Sayfalama, süzme, sıralama, genişletme, idempotency ve yeniden deneme — hepsi tek lehçe.

Keyset sayfalama

Tek lehçe vardır. cursor opak bir dizedir: kaynağın sıralama alanı ile id’sinden oluşan tuple’ı taşır, siz onu çözmez, yalnız geri verirsiniz. page_size varsayılan 25, tavan 100’dür ve tavanı aşan değer sessizce kırpılır; 1’den küçük ya da sayı olmayan değer ve çözülemeyen bir cursor 400 validation_failed alır. meta.next_cursor null ise o sayfa sonuncudur; kayıtlar tam dolu bir sayfada bitse de son sayfa null taşır, boş bir sonraki sayfa istemeniz gerekmez.

HTTP
# İlk sayfa
GET /v1/contacts?page_size=100&sort=-created_at

# Yanıt
{
  "data": [ … ],
  "meta": { "next_cursor": "MjAyNi0wMy0xNVQxMTowMDowMFp8…" }
}

# Sonraki sayfa — cursor AYNEN geri verilir, çözülmeye çalışılmaz
GET /v1/contacts?page_size=100&sort=-created_at&cursor=MjAyNi0wMy0xNVQxMTowMDowMFp8…

total alanı yoktur ve eklenmeyecektir: keyset ile doğru bir toplam ancak ikinci bir COUNT sorgusuyla verilebilirdi, o da sayfalamanın ucuz olma nedenini yok ederdi. Sayfa numarası (page=) da yoktur.

Süzme — filter[alan][operatör]

Operatör açıkça yazılır; operatörsüz filter[alan] biçimi tanınmaz — “eşitlik mi, içerir mi, aralık mı” sorusunu yanıtsız bırakan tam olarak o biçimdir.

OperatörAnlamı
gtebüyük veya eşit
lteküçük veya eşit
gtbüyük
ltküçük
eqeşit

Hangi alanın hangi operatörleri kabul ettiği kaynak başına beyaz listedir ve sözleşmede x-filter altında yayımlanır. Zaman alanlarında değer RFC3339’dur (saat farkındaki +, %2B olarak yazılır). eq yalnız iki tür alanda açıktır: sabit sözlüklü alanlar (örn. filter[payment_status][eq]=paid; sözlük dışı değer 400) ve kimlik alanları (örn. stok hareketlerinde filter[product_id][eq]=<uuid>). Tanınmayan alan, operatör ya da biçim sessizce yok sayılmaz: 400 validation_failed döner — süzülmemiş bir listeyi süzülmüş sanmanız imkânsızdır. Aynı süzgeci iki kez göndermek de 400’dür.

HTTP
GET /v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&filter[created_at][lt]=2026-02-01T00:00:00Z

⚠ Parantezler kodlanmadan gider ve imza ham sorgu dizesini alır — bkz. İmzalama.

Sıralama

sort=alan (artan) veya sort=-alan (azalan), tek alan. Beyaz liste kaynak başınadır (x-sort) ve dardır: cursor (sıra alanı, id) tuple’ı olduğu için keyset yalnız o anahtar üzerinde tutarlıdır. Verilmezse kaynağın varsayılanı uygulanır (x-sort-default): çoğu listede -created_at, tarihli defterlerde (cari ekstresi, stok hareketleri, kasa/banka hareketleri, kurlar) -date. Beyaz liste dışı bir alan 400 alır.

Genişletme — include

include=ad[,ad2] ilişkiyi satır içi genişletir ve derinlik 1’dir; include=contact.address gibi noktalı bir ad 400 alır. JSON:API’nin included[] yan yükü alınmadı: yanıt invoice.contact = {…} şeklindedir, istemci tarafında birleştirme yoktur. Faturanın kalemleri gibi gömülü olan şey bir ilişki değildir, her zaman gövdededir.

Eşleşme yoksa alan hiç gelmez. include=contact istediğiniz bir faturada cari kartı bulunamazsa yanıtta contact anahtarı bulunmaz — null da gelmez. Bu bir hata değildir ve olağandır: cari kartı açılmamış bir alıcıya da fatura kesilir; belgenin kendi contact_title ve tax_number alanları her zaman durur. İstemciniz “anahtar var ama null” varsayımıyla yazılmışsa bu satırlarda patlar.

Belge uçlarında include=contact ayrıca cari kapsamını ister: kapsamı taşımayan anahtar 403 alır, genişletmesiz istek etkilenmez. Bu, her genişletmenin kuralı değildir — include=category stok kapsamında kalır, /v1/me ise hiçbir kapsam sormaz.

Yazma uçlarında doğrulama

Gövde en fazla 2 MiB olabilir. Sözleşmede olmayan gövde alanı yok sayılır; tanımlı bir alanın kuralını karşılamayan değer ise 400 validation_failed alır ve details.field o alanın istekteki adını taşır (tax_number, items[1].product_id gibi). Sık karşılaşılan kurallar:

  • Sözlük alanları (cari type/person_type, ürün type, fatura invoice_type/scenario) yalnız sözleşmedeki değerleri alır.
  • Cari tax_number 10 haneli VKN, national_id 11 haneli TCKN olmalı ve kontrol basamağı tutmalıdır (nihai tüketici 11111111111 ve yurt dışındaki alıcı 2222222222 hariç). Kişi email biçimi denetlenir.
  • Ürün unit sözleşmedeki birim kodlarından biri ya da firmanın aktif bir birimidir; currency büyük harfli ISO 4217 kodudur; vat_rate 0, 1, 8, 10, 18 ya da 20 olabilir (fatura kaleminde de); unit_price negatif olamaz.

Benzersiz olması gereken bir değer (VKN, ürün kodu, kategori adı) zaten kullanılıyorsa yanıt 409 conflict’tir; etiket adları benzersiz değildir.

Idempotency

Idempotency-Key başlığı yalnız POST /v1/invoices/{id}/send ucunda okunur: aynı anahtarla ikinci istek saklanan yanıtı döndürür ve GİB’e ikinci bir gönderim olmaz. Aynı anahtarı başka bir istek için kullanmak 422 idempotency_key_reuse verir — her mantıksal işlem için yeni bir UUID üretin.

Kayıt oluşturan uçlarda (POST /v1/contacts, POST /v1/products, POST /v1/invoices, …) başlık okunmaz: yeniden gönderim ikinci bir kayıt yaratır (aynı VKN/TCKN’li cari kartı ise 409 conflict alır). Aynısı POST /v1/invoices/{id}/payments için de geçerlidir ve orada bedeli yüksektir: tekrarlanan istek ikinci bir ödeme satırı yazar. Yeniden denemeden önce GET /v1/invoices/{id}/payments ile defteri okuyun. Gönderim ucu anahtarsız da çift göndermez, çünkü belge artık taslak olmadığından 409 alır.

Oran sınırı ve yeniden deneme

Tavan anahtar başına dakikada 60 istektir; jetonlu kova doludur, yani 60 isteklik bir yığın tek seferde geçer, sonrası saniyede bir istektir. İmzası doğrulanmış her yanıt X-RateLimit-Limit, X-RateLimit-Remaining ve X-RateLimit-Reset taşır; 429 ayrıca Retry-After verir. Kotayı istemci tarafında sabit bir sayıyla taklit etmeyin — başlıkların var olma sebebi budur.

Yeniden denenebilir olanlar: 408, 429 ve 5xx. Diğer 4xx yanıtları tekrar denemekle düzelmez.

Sözde kod
deneme = 0
while deneme < 5:
    yanit = istek_at()
    if yanit.status == 429:
        bekle(yanit.headers["Retry-After"])          # sunucunun verdiği saniye
    elif yanit.status in (408, 500, 502, 503, 504):
        bekle(min(2 ** deneme, 60) + rastgele(0, 1)) # üstel + jitter
    else:
        break                                        # 4xx: tekrar deneme
    deneme += 1