e-Fatura'ya geçiş paketi: 1 yıl Ön Muhasebe + e-Fatura + 1 yıllık e-imza 4.750 TL + KDV → İncele
Kayıt olmadan dene: örnek işletmede faturadan stoğa, pazaryerinden kasaya her ekranı gezKayıt olmadan örnek işletmeyi gez → Demoyu dene
Uygun Muhasebe API · v1

Satış olunca muhasebesi kendiliğinden işlensin.

Web siten, e-ticaret altyapın ya da kendi programın satışı tek bir istekle Uygun Muhasebe'ye gönderir: müşteri kartı açılır, satış deftere yazılır, tahsilat ve stok işlenir. İstersen fatura e-Fatura ya da e-Arşiv olarak da gider, müşterine e-postayla ulaşır. Uygun Muhasebe'nin kendi abonelik ve e-imza satışlarının faturası da bu yoldan kesilir.

Her pakette dahil Test anahtarıyla dene İmzalı webhook'lar
WooCommerce mağazanı kod yazmadan bağlamak için API'ye gerek yok: Uygun Muhasebe'de Pazaryeri > Mağaza bağla > WooCommerce'u seç, sitenin REST API anahtarlarını yapıştır; ödemesi alınan siparişler gelir, her birine fatura taslağı açılır (kontrol edip toplu onaylarsın), ürünler SKU ile eşleşir, e-Arşiv'e internet satışı bilgileri (site adresi, ödeme şekli, kargo) kendiliğinden yazılır. API, kendi yazılımını bağlayacaklar için. İkisini aynı anda kullanma: aynı sipariş iki yoldan gelmesin.

Başlarken

  1. Uygun Muhasebe'de Ayarlar > API sayfasını aç (işletme sahibi ya da yönetici). API her pakette dahildir.
  2. Önce bir test anahtarı oluştur: test anahtarıyla kesilen fatura taslak kalır, numara almaz, gönderilmez; panelden silersin. Bağlantın çalışınca canlı anahtar oluştur.
  3. Anahtar yalnız oluşturulduğu an görünür. Sunucundaki ayar dosyasında sakla; tarayıcı koduna, uygulama paketine koyma.

Bütün istekler https://uygunmuhasebe.com/api/v1 altına gider, gövde ve yanıt UTF-8 JSON'dur. Her isteğe anahtarını başlık olarak ekle:

Authorization: Bearer tzk_canli_xxxxxxxxxxxxxxxx
Content-Type: application/json

Bağlantıyı denemek için:

curl https://uygunmuhasebe.com/api/v1/ping -H "Authorization: Bearer $UYGUN_MUHASEBE_ANAHTARI"

{"firma": "Örnek Ticaret Ltd. Şti.", "anahtar": "Web sitesi", "test": true, "e_belge": true, "kayit_turu": "e_belge"}

Anahtar onu oluşturan kişinin adına çalışır: kişinin satış faturalarında tam yetkisi yoksa ya da işletmeden çıkarılırsa anahtar da yazamaz. Kullanmadığın anahtarı Ayarlar > API'den iptal et. Windows'ta curl Türkçe harfleri bozabilir: gövdeyi UTF-8 bir dosyaya yazıp --data-binary @siparis.json ile gönder.

Kayıt türleri

Anahtarı oluştururken satış gelince ne yapılacağını seçersin; istekte e_belge_gonder ile tek seferlik değişir.

TürNe olur?Kimin için
Fatura kes ve gönderFatura onaylanır, Uygun Muhasebe serisinden numara alır, alıcıya göre e-Fatura ya da e-Arşiv olarak GİB'e gider; e-Arşiv müşterinin e-postasına ulaşır.e-Fatura olan paket
Yalnız muhasebeye kaydetFatura başka yerde kesildiyse (ör. e-ticaret altyapının kendi e-Arşivi) numarasını belge_no ile gönderirsin: satış o numarayla deftere yazılır, Uygun Muhasebe göndermez. e-Belge kullanmayan işletmede belge_no gerekmez; faturaya kendi serinden numara verilir.Her paket

İki türde de müşteri kartı, cari bakiye, gelir, tahsilat ve (satırda urun_kodu varsa) stok işlenir.

Fatura kes POST /faturalar

Müşteri vergi numarasıyla, yoksa e-postasıyla, o da yoksa ünvan ve ille bulunur; bulunamazsa yeni müşteri kartı açılır. Canlı anahtarla satış onaylanıp deftere yazılır; kayıt türü "fatura kes ve gönder" ise e-Fatura ya da e-Arşiv olarak gider.

curl https://uygunmuhasebe.com/api/v1/faturalar \
  -H "Authorization: Bearer $UYGUN_MUHASEBE_ANAHTARI" \
  -H "Content-Type: application/json" \
  -d '{
    "siparis_no": "1001",
    "alici": {
      "unvan": "Ayşe Yılmaz",
      "vergi_no": "",
      "eposta": "ayse@ornek.com",
      "telefon": "05xx xxx xx xx",
      "adres": "Atatürk Cad. No: 1",
      "ilce": "Kadıköy",
      "il": "İstanbul"
    },
    "satirlar": [
      {"aciklama": "Seramik kupa", "miktar": 2, "birim": "Adet", "birim_fiyat": "250.00", "kdv_orani": 20},
      {"aciklama": "Kargo", "miktar": 1, "birim_fiyat": "50.00", "kdv_orani": 20}
    ],
    "kdv_dahil": false,
    "tahsilat_hesabi_id": 3
  }'
AlanAçıklama
alici.unvan zorunluŞirket ünvanı ya da kişinin adı soyadı (en çok 255 karakter).
alici.vergi_no10 haneli VKN ya da 11 haneli TCKN (yalnız rakam). Boşsa e-Arşiv faturası nihai tüketiciye kesilir. Sen e-Fatura mükellefiysen ve alıcı GİB'de e-Fatura kayıtlı kullanıcısıysa e-Fatura, değilse e-Arşiv gider.
alici.epostae-Arşiv faturası bu adrese gönderilir.
alici.vergi_dairesi, telefon, adres, ilce, ilFaturada görünür; müşteri kartında eksikse doldurulur.
alici.kurumsaltrue / false. Verilmezse 10 haneli VKN kurumsal, 11 haneli TCKN şahıs sayılır.
satirlar zorunlu1–200 satır. Her satırda aciklama ve birim_fiyat zorunlu; miktar (varsayılan 1, en az 0,001), birim (varsayılan Adet), kdv_orani (0, 1, 10, 20; varsayılan 20), urun_kodu (Uygun Muhasebe'deki ürünün stok kodu ya da barkodu: satır ürüne bağlanır, stok düşer; böyle bir ürün yoksa istek 400 döner, önce ürünü ekle).
kdv_dahiltrue ise birim fiyatlar KDV dahildir, KDV ayrıştırılır.
tarihYYYY-AA-GG. Verilmezse bugün; ileri tarih olamaz.
siparis_noSenin sipariş numaran (en çok 50 karakter); faturada görünür, listede bununla ararsın. Aynı sipariş numarasıyla ikinci fatura kesilmez (çift kaydı önleme).
para_birimiYalnız TRY (verilmezse TRY). Başka para birimiyle gelen istek 400 döner, fatura kesilmez: dövizli faturayı panelden kes.
notlarFaturanın not alanı.
belge_noFatura başka yerde kesildiyse numarası (en çok 30 karakter): satış bu numarayla yalnız muhasebeye yazılır, e-Belge gönderilmez.
e_belge_gondertrue / false: anahtarın kayıt türünü bu istek için değiştirir. e-Belge kullanan işletmede false ise belge_no gerekir.
tahsilat_hesabi_idÖdeme alındıysa kasa / banka hesabının kimliği (GET /hesaplar): faturanın tamamı tahsil edilmiş işlenir.

Yanıt 201, kesilen fatura:

{
  "id": 4821,
  "belge_no": "EAR2026000000123",
  "durum": "onayli",
  "tarih": "2026-10-13",
  "vade_tarihi": null,
  "siparis_no": "1001",
  "para_birimi": "TRY",
  "ara_toplam": "550.00",
  "iskonto_toplam": "0.00",
  "kdv_toplam": "110.00",
  "genel_toplam": "660.00",
  "tahsil_edilen": "660.00",
  "kalan": "0.00",
  "alici": {"id": 311, "unvan": "Ayşe Yılmaz", "vergi_no": "", "eposta": "ayse@ornek.com"},
  "baska_yerde_kesildi": false,
  "e_belge": {"tur": "earsiv", "durum": "gonderildi", "durum_aciklamasi": "Gönderildi, işlenmeyi bekliyor",
              "belge_no": "EAR2026000000123", "ettn": "…", "alici_yaniti": "", "hata": "",
              "gonderim_zamani": "2026-10-13T11:05:12.408211+00:00"},
  "goruntuleme_adresi": "https://uygunmuhasebe.com/p/…/",
  "pdf_adresi": "https://uygunmuhasebe.com/p/…/fatura.pdf",
  "olusturma": "2026-10-13T11:05:11.913542+00:00",
  "satirlar": [{"aciklama": "Seramik kupa", "miktar": "2", "birim": "Adet", "birim_fiyat": "250",
                "kdv_orani": "20", "kdv": "100.00", "toplam": "600.00"}, …]
}

Tutarlar yuvarlama kaybı olmasın diye metin olarak döner; zamanlar ISO 8601, UTC'dir. e-Belge gönderimi fatura kaydedildikten hemen sonra yapılır: yanıttaki e_belge null olabilir ya da henüz son durumunu göstermeyebilir; sonucunu GET /faturalar/{id} ile ya da webhook'la öğrenirsin. goruntuleme_adresi giriş gerektirmeyen fatura sayfasıdır, müşterine sipariş e-postanda da verebilirsin. pdf_adresi e-Belge GİB'e gittiyse entegratördeki resmî PDF'i açar, PDF henüz hazır değilse fatura sayfasına yönlendirir. Test anahtarıyla durum taslak olur, adresler boş döner.

Faturaları oku GET /faturalar

GET /faturalar?siparis_no=1001 siparişin faturasını bulur. durum (taslak, onayli, iptal) ve sayfa ile süzülür; sayfada 50 fatura, en yenisi önce:

{"sonuclar": [{…fatura, satirlar hariç…}], "sayfa": 1, "sonraki_sayfa": 2}

GET /faturalar/{id} tek faturayı satırları ve e-Belge durumuyla döner. e_belge.durum:

durumAnlamı
taslakHenüz GİB'e gönderilmedi
kuyruktaGönderim sırasında
gonderildiGönderildi, işlenmeyi bekliyor
basariliBaşarıyla tamamlandı
hataHata
iptalİptal edildi

e_belge.hata durum hata iken nedeni yazar (ör. eksik bilgi); faturayı Uygun Muhasebe'de düzeltip yeniden gönderirsin. alici_yaniti ticari e-Faturada alıcının yanıtıdır (bekliyor, kabul, kismi, red); e-Arşivde boştur.

Tahsilat işle POST /faturalar/{id}/tahsilat

Ödemesi sonradan gelen faturaya tahsilat ekler (canlı anahtar). hesap_id zorunlu; tutar verilmezse kalan borcun tamamı, tarih verilmezse bugün; aciklama isteğe bağlı.

curl https://uygunmuhasebe.com/api/v1/faturalar/4821/tahsilat \
  -H "Authorization: Bearer $UYGUN_MUHASEBE_ANAHTARI" -H "Content-Type: application/json" \
  -d '{"hesap_id": 3, "tutar": "660.00"}'

{"tahsilat": {"id": 912, "tutar": "660.00", "tarih": "2026-10-13", "hesap_id": 3, "makbuz_no": "MKB2026000000045"},
 "fatura": {…}}

GET /hesaplar kasa ve banka hesaplarını listeler: {"sonuclar": [{"id": 3, "ad": "İş Bankası", "tur": "banka", "para_birimi": "TRY"}]}

Ürünler GET · POST /urunler

Faturada urun_kodu gönderip stok düşürmek için ürünün Uygun Muhasebe'de aynı stok koduyla (ya da barkodla) kayıtlı olması gerekir. GET /urunler?kod=9786050000001 faturadaki aramanın aynısını yapar (önce stok kodu, sonra barkod); kod verilmezse bütün ürünler sayfa sayfa döner.

curl https://uygunmuhasebe.com/api/v1/urunler \
  -H "Authorization: Bearer $UYGUN_MUHASEBE_ANAHTARI" -H "Content-Type: application/json" \
  -d '{"ad": "Örnek Roman", "kod": "9786050000001", "satis_fiyati": "250.00", "kdv_orani": 20}'

{"id": 77, "ad": "Örnek Roman", "kod": "9786050000001", "barkod": "", "tur": "urun", "birim": "Adet",
 "kdv_orani": "20", "satis_fiyati": "250", "para_birimi": "TRY", "stok_takibi": true}

ad zorunlu; kod, barkod (en çok 50 karakter), tur (urun, hizmet), birim (Adet, Kg, Lt, Paket…), kdv_orani (0, 1, 10, 20), satis_fiyati (KDV hariç; kdv_dahil: true ile KDV dahil verilir), stok_takibi (hizmette varsayılan false). Aynı kod ya da barkodla ürün varsa yeni ürün açılmaz: 409 urun_var ve var olan ürün "urun" alanında döner. Anahtarı oluşturanın ürünlerde tam yetkisi olmalı.

Müşteriler GET · POST /cariler

Fatura keserken müşteri kendiliğinden bulunur ya da açılır; ayrıca sormak istersen GET /cariler?vergi_no=1234567890 (ya da ?eposta=). POST /cariler faturadaki alici ile aynı alanları alır, aynı kuralla bulur ya da açar: yeni açıldıysa 201, bulunduysa 200.

{"id": 311, "unvan": "Örnek Dağıtım A.Ş.", "tur": "musteri", "kurumsal": true, "vergi_no": "1234567890",
 "vergi_dairesi": "Kadıköy", "eposta": "muhasebe@ornek.com", "telefon": "", "adres": "", "ilce": "Kadıköy", "il": "İstanbul"}

Çift kaydı önleme

  • Sipariş numarası: siparis_no ile kesilmiş bir fatura varsa aynı numarayla yeni fatura kesilmez; yanıt 409 siparis_faturalandi olur ve var olan fatura "fatura" alanında döner, onu kaydedip devam edebilirsin. İptal edilen fatura sayılmaz. Bir siparişi parça parça faturalıyorsan numarayı ayır (ör. 1001-1, 1001-2). Test anahtarının taslakları canlı anahtarı engellemez.
  • Idempotency-Key: bağlantı koptuğunda isteği yeniden göndermek güvenli olsun diye POST isteklerine Idempotency-Key başlığı ekle. Aynı anahtarla gelen ikinci istek işlenmez, ilk yanıt Idempotent-Replayed: true başlığıyla aynen döner. Aynı anahtar farklı gövdeyle gelirse 409 tekrar_anahtari_kullanildi döner. Anahtarlar 30 gün saklanır; yalnız başarılı yanıtlar saklanır, hatalı isteği düzeltip aynı anahtarla gönderebilirsin.

Hatalar

{"hata": {"kod": "gecersiz_istek", "mesaj": "1. satırda KDV oranı 0, 1, 10 ya da 20 olmalı."}}
HTTPkodNe zaman
400gecersiz_istekEksik ya da hatalı alan, JSON ya da UTF-8 olmayan gövde; mesaj neyin düzeltileceğini söyler.
401anahtar_gecersizAnahtar yok, yanlış ya da iptal edilmiş.
403yetki_yokAnahtarı oluşturan kişinin yetkisi yetmiyor.
403salt_okunurAbonelik sona erdi; yalnız okuma çalışır.
403modul_kapaliSatış modülü kapalı.
403test_anahtariTest anahtarıyla yapılamayan işlem (tahsilat).
404bulunamadi, adres_yokKayıt ya da adres yok.
405yontem_yokAdres bu yöntemi kabul etmiyor; Allow başlığı kabul edilenleri söyler.
409siparis_faturalandiSiparişin faturası zaten var; fatura "fatura" alanında döner.
409urun_varAynı kod ya da barkodla ürün var; ürün "urun" alanında döner.
409tekrar_anahtari_kullanildiIdempotency-Key başka bir istekte kullanıldı.
429cok_istekDakikalık sınır aşıldı; Retry-After saniye sonra yeniden dene.
500sunucu_hatasiBeklenmeyen hata; ekibimize iletilir. Yeniden göndermeden önce kaydın oluşup oluşmadığına bak.

Webhook'lar

Ayarlar > API'den bir https adresi ekle; faturanın e-Belge durumu değişince adresine POST gelir:

OlayNe zaman
fatura.gonderildiFatura e-Fatura / e-Arşiv olarak gönderildi
fatura.hataFatura gönderilemedi
fatura.kabulAlıcı e-Faturayı kabul etti
fatura.redAlıcı e-Faturayı reddetti
fatura.iptalFatura iptal edildi
denemeAyarlar > API'de Deneme gönder'e bastın; veri yalnız bir mesajdır.
POST /wp-json/uygun-muhasebe/v1/webhook
Content-Type: application/json; charset=utf-8
User-Agent: UygunMuhasebe-Webhook/1.0
X-UygunMuhasebe-Olay: fatura.gonderildi
X-UygunMuhasebe-Teslim: 5531
X-UygunMuhasebe-Zaman: 1791896711
X-UygunMuhasebe-Imza: sha256=6f1c…

{"id": 5531, "olay": "fatura.gonderildi", "olusturma": "2026-10-13T11:05:13.120044+00:00", "veri": {…fatura…}}

veri, GET /faturalar/{id}'nin döndüğü faturadır (olay anındaki son hali). İmzayı doğrula: Ayarlar > API'deki imza anahtarıyla X-UygunMuhasebe-Zaman + "." + ham gövde metninin HMAC-SHA256'sını hesapla, başlıktakiyle sabit sürede karşılaştır; 5 dakikadan eski zamanı reddet. Gövdeyi JSON olarak çözüp yeniden yazma: imza gelen baytlar üzerindendir.

# Python
import hashlib, hmac, time

def dogru_mu(gizli: str, zaman: str, govde: bytes, imza_basligi: str) -> bool:
    beklenen = hmac.new(gizli.encode(), zaman.encode() + b"." + govde, hashlib.sha256).hexdigest()
    return zaman.isdigit() and abs(time.time() - int(zaman)) < 300 and hmac.compare_digest("sha256=" + beklenen, imza_basligi)
// PHP
$govde = file_get_contents('php://input');
$zaman = $_SERVER['HTTP_X_UYGUNMUHASEBE_ZAMAN'] ?? '';
$imza = $_SERVER['HTTP_X_UYGUNMUHASEBE_IMZA'] ?? '';
$beklenen = 'sha256=' . hash_hmac('sha256', $zaman . '.' . $govde, getenv('UYGUN_MUHASEBE_WEBHOOK_GIZLI'));
$dogru = ctype_digit($zaman) && abs(time() - (int) $zaman) < 300 && hash_equals($beklenen, $imza);
  • Yanıt: 10 saniye içinde 2xx dön, işini sonra yap. 2xx dışındaki her yanıt (404, 500, giriş sayfası…) başarısız sayılır. 2xx ile bir HTML sayfası dönerse teslim edildi sayılır ama teslim kaydında uyarı çıkar: adres büyük ihtimalle olayı işleyen kod değil, sitenin bir sayfasıdır.
  • Yeniden deneme: başarısız teslim 15 dakika, 30 dakika, 1, 3, 6 ve 12 saat arayla yeniden denenir (en çok 7 deneme). Aynı olay iki kez gelebilir: id ile tekrarları ayıkla.
  • Yönlendirme izlenmez: adresin başka adrese yönlendiriyorsa (ör. www'suz adrese ya da sonda / olan adrese 301) teslim başarısız sayılır. Teslim kaydında yönlendirdiği adres yazar; webhook adresini onunla değiştir.
  • Adres: https:// ile başlamalı ve herkese açık bir sunucu olmalı: localhost, iç ağ (10.x, 172.16–31.x, 192.168.x) ve bulut meta veri adresleri (169.254.x) kabul edilmez.
  • Teslim kaydı: Ayarlar > API'de son teslimler, deneme sayısı, yanıt kodu ve sorunun açıklamasıyla görünür. Bağlantıyı kurarken Deneme gönder ile dene: sonuç hemen ekrana gelir.

WordPress / WooCommerce

WooCommerce siparişlerini kendi kurallarınla göndermek istiyorsan (siparişe eklediğin vergi no alanı, kendi fatura kuralların) bu iki parçayla başla; ikisi aynı eklenti dosyasına girer. Kod yazmadan bağlamak için yukarıdaki nota bak.

1. Sipariş tamamlanınca fatura kes. Sipariş "Tamamlandı" olunca WooCommerce'in Action Scheduler'ı faturayı arka planda gönderir. Sipariş numarası siparis_no olur: sipariş ikinci kez tamamlanırsa yeni fatura kesilmez, 409 ile ilk fatura döner ve eklenti onu kaydeder. Fatura numarası ve bağlantısı sipariş notuna, kimliği ve PDF adresi siparişin özel alanlarına yazılır; hata olursa nedeni sipariş notuna düşer. Fatura kesildi ama e-Belge gönderilemediyse (ör. %0 KDV'de muafiyet sebebi) bu da nota yazılır.

<?php
/**
 * Plugin Name: Uygun Muhasebe fatura
 * Description: Tamamlanan WooCommerce siparişinin faturasını Uygun Muhasebe'de keser.
 *
 * wp-content/plugins/uygun-muhasebe/uygun-muhasebe.php olarak kaydet, Eklentiler'den etkinleştir.
 * wp-config.php'ye anahtarını yaz (Ayarlar > API'de oluşturduğun anahtar):
 *   define( 'UYGUN_MUHASEBE_ANAHTARI', 'tzk_canli_...' );
 */

if ( ! defined( 'UYGUN_MUHASEBE_API' ) ) {
    define( 'UYGUN_MUHASEBE_API', 'https://uygunmuhasebe.com/api/v1' );
}
if ( ! defined( 'UYGUN_MUHASEBE_KDV_ORANI' ) ) {
    define( 'UYGUN_MUHASEBE_KDV_ORANI', 20 ); // WooCommerce'te vergi kapalıysa fiyatlar KDV dahil, bu oranla
}

// Ödeme alınınca kesmek istersen: woocommerce_order_status_processing
add_action( 'woocommerce_order_status_completed', 'uygun_muhasebe_siraya_al' );
add_action( 'uygun_muhasebe_fatura_kes', 'uygun_muhasebe_fatura_kes' );

// Siparişi tamamlayan ekran beklemesin: WooCommerce'in Action Scheduler'ı arka planda gönderir.
function uygun_muhasebe_siraya_al( $order_id ) {
    if ( function_exists( 'as_enqueue_async_action' ) ) {
        as_enqueue_async_action( 'uygun_muhasebe_fatura_kes', array( $order_id ), 'uygun-muhasebe' );
    } else {
        uygun_muhasebe_fatura_kes( $order_id );
    }
}

function uygun_muhasebe_fatura_kes( $order_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order || $order->get_meta( '_uygun_muhasebe_fatura_id' ) ) {
        return; // fatura zaten kesildi
    }
    // Vergi açıksa satırın oranı WooCommerce'in hesapladığı vergiden gelir (vergisiz satır %0);
    // kapalıysa fiyatlar KDV dahildir, oranı UYGUN_MUHASEBE_KDV_ORANI.
    $vergili  = wc_tax_enabled();
    $kalemler = $order->get_items( array( 'line_item', 'shipping', 'fee' ) );
    // Eksi tutarlı ücret (ör. havale indirimi) satırlara tutarlarıyla orantılı dağıtılır.
    $arti = 0;
    $eksi = 0;
    foreach ( $kalemler as $kalem ) {
        $tutar = (float) $kalem->get_total() + (float) $kalem->get_total_tax();
        if ( $tutar < 0 ) {
            $eksi -= $tutar;
        } else {
            $arti += $tutar;
        }
    }
    if ( $arti <= $eksi ) {
        $order->add_order_note( 'Uygun Muhasebe: siparişin tutarı 0, fatura kesilmedi.' );
        return;
    }
    $carpan   = 1 - $eksi / $arti;
    $satirlar = array();
    foreach ( $kalemler as $kalem ) {
        $adet = max( 1, (float) $kalem->get_quantity() );
        $net  = (float) $kalem->get_total(); // indirim düşülmüş; vergi açıksa KDV hariç
        if ( $net <= 0 ) {
            continue; // ücretsiz kargo, eksi tutarlı ücret
        }
        $oran = $vergili ? uygun_muhasebe_oran( (float) $kalem->get_total_tax() / $net * 100 ) : UYGUN_MUHASEBE_KDV_ORANI;
        $satirlar[] = array(
            'aciklama'    => $kalem->get_name(),
            'miktar'      => $adet,
            'birim_fiyat' => number_format( $net * $carpan / $adet, 4, '.', '' ),
            'kdv_orani'   => $oran,
            // Stok da düşsün istersen: 'urun_kodu' => ürünün SKU'su. Ürün Uygun Muhasebe'de aynı stok
            // koduyla kayıtlı olmalı (POST /urunler), yoksa istek 400 döner.
        );
    }
    $iller = WC()->countries->get_states( $order->get_billing_country() );
    $il    = $order->get_billing_state(); // TR34 -> İstanbul
    $unvan = $order->get_billing_company();
    if ( ! $unvan ) {
        $unvan = $order->get_formatted_billing_full_name();
    }
    $govde = array(
        'siparis_no'  => (string) $order->get_order_number(), // aynı sipariş ikinci kez faturalanmaz (409)
        'para_birimi' => $order->get_currency(), // TRY değilse 400 döner, nedeni sipariş notuna düşer
        'kdv_dahil'   => ! $vergili,
        'alici'       => array(
            'unvan'         => $unvan,
            // Fatura eklentinin vergi no alanı; boşsa e-Arşiv nihai tüketiciye kesilir.
            'vergi_no'      => preg_replace( '/\D/', '', (string) $order->get_meta( '_billing_vergi_no' ) ),
            'vergi_dairesi' => (string) $order->get_meta( '_billing_vergi_dairesi' ),
            'eposta'        => $order->get_billing_email(),
            'telefon'       => $order->get_billing_phone(),
            'adres'         => trim( $order->get_billing_address_1() . ' ' . $order->get_billing_address_2() ),
            'ilce'          => $order->get_billing_city(),
            'il'            => isset( $iller[ $il ] ) ? $iller[ $il ] : $il,
        ),
        'satirlar'    => $satirlar,
    );
    $yanit = wp_remote_post( UYGUN_MUHASEBE_API . '/faturalar', array(
        'timeout' => 30,
        'headers' => array(
            'Authorization' => 'Bearer ' . UYGUN_MUHASEBE_ANAHTARI,
            'Content-Type'  => 'application/json',
        ),
        'body'    => wp_json_encode( $govde ),
    ) );
    if ( is_wp_error( $yanit ) ) {
        $order->add_order_note( 'Uygun Muhasebe: bağlanılamadı (' . $yanit->get_error_message() . ').' );
        return; // siparişi yeniden "Tamamlandı" yapınca tekrar denenir
    }
    $kod  = (int) wp_remote_retrieve_response_code( $yanit );
    $veri = json_decode( wp_remote_retrieve_body( $yanit ), true );
    if ( 409 === $kod && isset( $veri['fatura'] ) ) {
        $veri = $veri['fatura']; // bu sipariş daha önce faturalanmış: o faturayı kaydet
    } elseif ( 201 !== $kod ) {
        $mesaj = isset( $veri['hata']['mesaj'] ) ? $veri['hata']['mesaj'] : 'yanıt okunamadı';
        $order->add_order_note( 'Uygun Muhasebe: fatura kesilemedi (' . $kod . '): ' . $mesaj );
        return;
    }
    if ( 'taslak' === $veri['durum'] ) {
        // Test anahtarı: fatura taslak kalır. Sipariş işaretlenmez; canlı anahtarla yeniden kesilir.
        $order->add_order_note( 'Uygun Muhasebe test faturası (taslak, id ' . $veri['id'] . '): canlı anahtara '
            . 'geçince sipariş yeniden "Tamamlandı" olduğunda gerçek fatura kesilir.' );
        return;
    }
    $order->update_meta_data( '_uygun_muhasebe_fatura_id', $veri['id'] );
    $order->update_meta_data( '_uygun_muhasebe_fatura_pdf', $veri['pdf_adresi'] );
    $order->save();
    $not = 'Uygun Muhasebe faturası: ' . $veri['belge_no'] . ' ' . $veri['goruntuleme_adresi'];
    if ( ! empty( $veri['e_belge']['hata'] ) ) { // ör. %0 KDV'de muafiyet sebebi: Uygun Muhasebe'de seçip gönder
        $not .= ' — e-Belge gönderilemedi: ' . $veri['e_belge']['hata'];
    }
    $order->add_order_note( $not );
}

// WooCommerce'in hesapladığı oranı Uygun Muhasebe'nin oranlarından en yakınına yuvarlar (0, 1, 10, 20).
function uygun_muhasebe_oran( $oran ) {
    $en_yakin = 20;
    foreach ( array( 0, 1, 10, 20 ) as $o ) {
        if ( abs( $oran - $o ) < abs( $oran - $en_yakin ) ) {
            $en_yakin = $o;
        }
    }
    return $en_yakin;
}
  • WooCommerce'te vergi no alanı yoktur: fatura eklentinin alan adını yaz (örnekte _billing_vergi_no); boşsa fatura nihai tüketiciye e-Arşiv olarak kesilir.
  • KDV: WooCommerce'te vergi açıksa her satırın oranı WooCommerce'in hesapladığı vergiden gelir; vergisi 0 olan satır (ör. "Sıfır oran" sınıfındaki ürün) %0 gider, bu yüzden vergiyi açtıysan oranları da gir. Vergi kapalıysa fiyatlar KDV dahil sayılır ve hepsine UYGUN_MUHASEBE_KDV_ORANI yazılır; farklı oranlı ürünlerin varsa (ör. kitap ve kırtasiye) vergiyi aç.
  • İndirim: kupon indirimi satır tutarlarında zaten düşülüdür. Havale indirimi gibi eksi tutarlı ücretler satırlara tutarlarıyla orantılı dağıtılır; fatura müşterinin ödediği tutarı verir (satır yuvarlamasından kuruş farkı olabilir).
  • Test anahtarı: kesilen taslak siparişi işaretlemez, yalnız sipariş notuna yazılır. Canlı anahtara geçince sipariş yeniden "Tamamlandı" olduğunda gerçek fatura kesilir.
  • TL dışındaki para birimiyle verilen sipariş faturalanmaz; nedeni sipariş notuna düşer.
  • Sipariş sonradan iptal ya da iade edilirse fatura kendiliğinden iptal edilmez: e-Arşiv'i 8 gün içinde Uygun Muhasebe'de iptal edersin, sonrası için iade faturası kesersin.

2. Webhook'u karşıla. e-Belge durumu değişince (gönderildi, hata, iptal…) siparişe not düşer. Webhook adresi olarak https://siten.com/wp-json/uygun-muhasebe/v1/webhook yaz.

<?php
/*
 * Uygun Muhasebe webhook'unu WordPress'te karşılar (aynı eklenti dosyasının sonuna ekle).
 * Webhook adresi: https://siten.com/wp-json/uygun-muhasebe/v1/webhook
 *   (kalıcı bağlantılar "Düz" ise: https://siten.com/?rest_route=/uygun-muhasebe/v1/webhook)
 * wp-config.php'ye webhook'un imza anahtarını yaz (Ayarlar > API > Webhook'lar > Göster):
 *   define( 'UYGUN_MUHASEBE_WEBHOOK_GIZLI', 'whsec_...' );
 */

add_action( 'rest_api_init', function () {
    register_rest_route( 'uygun-muhasebe/v1', '/webhook', array(
        'methods'             => 'POST',
        'callback'            => 'uygun_muhasebe_webhook',
        'permission_callback' => '__return_true', // isteği imza doğrular
    ) );
} );

function uygun_muhasebe_webhook( WP_REST_Request $istek ) {
    $govde    = $istek->get_body(); // ham gövde: imza bunun üzerinden hesaplanır
    $zaman    = (string) $istek->get_header( 'X-UygunMuhasebe-Zaman' );
    $imza     = (string) $istek->get_header( 'X-UygunMuhasebe-Imza' );
    $beklenen = 'sha256=' . hash_hmac( 'sha256', $zaman . '.' . $govde, UYGUN_MUHASEBE_WEBHOOK_GIZLI );
    $taze     = preg_match( '/^\d+$/', $zaman ) && abs( time() - (int) $zaman ) <= 300; // 5 dakikadan eski değil
    if ( ! $taze || ! hash_equals( $beklenen, $imza ) ) {
        return new WP_REST_Response( array( 'hata' => 'imza geçersiz' ), 401 );
    }
    $olay = json_decode( $govde, true );
    if ( get_transient( 'uygun_muhasebe_olay_' . $olay['id'] ) ) {
        return new WP_REST_Response( array( 'tamam' => true ), 200 ); // aynı olay ikinci kez geldi
    }
    set_transient( 'uygun_muhasebe_olay_' . $olay['id'], 1, DAY_IN_SECONDS );
    if ( 0 === strpos( $olay['olay'], 'fatura.' ) && ! empty( $olay['veri']['siparis_no'] ) ) {
        $no     = (string) $olay['veri']['siparis_no'];
        $order  = wc_get_order( (int) $no );
        $ebelge = $olay['veri']['e_belge'];
        // Sipariş numarası eklentisi varsa numara kimlikten farklıdır: numarası tutmayan siparişe not yazılmaz.
        if ( $order && (string) $order->get_order_number() === $no && $ebelge ) {
            $not = 'Uygun Muhasebe ' . $olay['veri']['belge_no'] . ' — ' . $ebelge['durum_aciklamasi'];
            $order->add_order_note( $ebelge['hata'] ? $not . ': ' . $ebelge['hata'] : $not );
        }
    }
    return new WP_REST_Response( array( 'tamam' => true ), 200 ); // 10 saniye içinde 2xx dön
}

Sipariş, faturadaki siparis_no ile bulunur. Sipariş numarası eklentisi kullanıyorsan (numara sipariş kimliğinden farklıysa) not yazılmaz, yanlış siparişe de düşmez; siparişi o eklentinin işleviyle bulacak şekilde değiştir.

Sınırlar

  • İşletme başına dakikada 120 istek; bir faturada en çok 200 satır; listelerde sayfada 50 kayıt.
  • Faturalar Türk lirasıyla kesilir. İskonto, tevkifat ve ÖTV gibi özel durumlar için faturayı panelden kes; indirimi satır fiyatına yansıt.
  • API'den kesilen e-Arşiv faturasına internet satışı bilgileri (satışın yapıldığı site adresi, ödeme şekli ve tarihi, kargo firması) yazılmaz. İnternetten satış yapıyorsan e-Arşiv faturada bu bilgiler istenir (509 sıra no.lu VUK Genel Tebliği): siten WooCommerce, Shopify, ikas ya da IdeaSoft ise Pazaryeri > Mağaza bağla yolunu kullan, orada kendiliğinden yazılır.
  • %0 KDV'li satır içeren fatura e-Belge'de muafiyet sebebi ister; API bunu almaz. Böyle bir faturanın e-Belge'si hata durumuna düşer (webhook: fatura.hata); Uygun Muhasebe'de faturayı açıp e-Belge bölümünde fatura tipini ve muafiyet sebebini seçerek gönderirsin.
  • e-Belge göndermek için e-Fatura olan paket (Ön Muhasebe + e-Fatura ya da Sadece e-Fatura), tamamlanmış e-Fatura geçişi ve e-kontör gerekir; e-kontör yoksa fatura onaylanır, e-Belge sonra gönderilir.
Takıldığın yerde bize yaz; isteğin gövdesini ve dönen hatayı eklersen hızlı yanıtlarız (anahtarını ekleme).