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.
# İ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ör | Anlamı |
|---|---|
gte | büyük veya eşit |
lte | küçük veya eşit |
gt | büyük |
lt | küçük |
eq | eş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.
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üntype, faturainvoice_type/scenario) yalnız sözleşmedeki değerleri alır. - Cari
tax_number10 haneli VKN,national_id11 haneli TCKN olmalı ve kontrol basamağı tutmalıdır (nihai tüketici11111111111ve yurt dışındaki alıcı2222222222hariç). Kişiemailbiçimi denetlenir. - Ürün
unitsözleşmedeki birim kodlarından biri ya da firmanın aktif bir birimidir;currencybüyük harfli ISO 4217 kodudur;vat_rate0, 1, 8, 10, 18 ya da 20 olabilir (fatura kaleminde de);unit_pricenegatif 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.
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