Ham anahtar yalnızca üretim anında bir kez gösterilir, geri okunamaz. Kaybederseniz yenisini üretip eskisini iptal edin. Anahtarı istemci tarafı koda koymayın — yalnızca kendi sunucunuzda tutun.
Geçersiz, iptal edilmiş ve süresi dolmuş anahtar aynı yanıtı alır:
401 Unauthorized
{
"error": {
"code": "unauthorized",
"message": "Geçersiz veya süresi dolmuş API anahtarı",
"field": null
}
}
İzinler
İzinler anahtar üretilirken seçilir ve sonradan değiştirilemez; yetkiyi değiştirmek için yeni anahtar üretip eskisini iptal edersiniz.
İzin
Ne yapar
orders:read
Sipariş listesi ve detayını okur.
orders:write
Siparişi onaylar (Hazırlanıyor).
deliveries:write
Dijital ürün kalemlerine teslimat işler.
invoices:write
Kesilen faturayı siparişe işler (yalnızca kurumsal satıcı).
products:write
Mağazaya ürün ekler, görsel yükler, yayına alır ve fiyat/stok/durum günceller. Ürün silmez.
403 Forbidden
{
"error": {
"code": "insufficient_scope",
"message": "Bu uç için gereken izin anahtarınızda yok: orders:write",
"field": null
}
}
Sayfalama
Liste uçları cursor ile sayfalanır. Yanıttaki nextCursor değerini bir sonraki isteğe ?cursor= olarak verin; null geldiğinde son sayfadasınız. limit varsayılan 25, en fazla 100.
Gövde doğrulaması, izin reddi, istek limiti ve iş kuralı hataları dahil tüm hatalar aynı yapıdadır. Koşullarınızı code alanına bağlayın; mesaj metni değişebilir. field hangi girdinin sorunlu olduğunu söyler.
Hata gövdesi
{
"error": {
"code": "invalid_status_transition",
"message": "Yalnızca ödemesi alınmış (PAID) sipariş onaylanabilir; bu sipariş PREPARING durumunda.",
"field": "status"
}
}
Kod
HTTP
Anlamı
unauthorized
401
Anahtar geçersiz, iptal edilmiş ya da süresi dolmuş.
insufficient_scope
403
Anahtarın bu uç için izni yok.
order_not_found
404
Sipariş bu mağazada yok.
item_not_found
404
Sipariş kalemi bulunamadı.
invalid_request
400
Parametre hatalı veya fazladan alan gönderildi.
invalid_status_transition
400
Sipariş bu durumdayken bu işlem yapılamaz.
not_digital_item
400
Kalem dijital değil.
order_not_paid
400
Ödeme alınmadan teslimat yapılamaz.
order_not_invoiceable
400
Sipariş faturalandırılabilir durumda değil.
seller_not_corporate
400
Fatura yalnızca kurumsal satıcıda işlenebilir.
rate_limited
429
İstek sınırı aşıldı.
internal_error
500
Beklenmeyen hata.
İstek limiti dakikada 600'dür ve anahtar başına sayılır.
POST
Ürün oluştur
POST/products
Gerekli izin:products:write
Ürünü taslak olarak açar. Taslak ürün mağaza sayfanızda görünmez ve sepete eklenemez; yayına çıkması için önce görsel yüklemeniz, sonra yayınlamanız gerekir.
Yanıt yalnızca Shophin'in ürettiği kimlikleri taşır: id, slug ve varyantlı üründe variants[].id. Gönderdiğiniz alanlar geri okunmaz.
Ortak alanlar
Alan
Zorunlu
Açıklama
title
evet
En fazla 140 karakter.
productType
evet
PHYSICAL veya DIGITAL
hasVariants
evet
Varyantlı üründe fiyat ve stok varyantlarda tutulur.
sku
hayır
Kendi stok kodunuz. Mağaza içinde tekil olmalı; ikinci kez gönderirseniz reddedilir.
vatRate
kurumsalda evet
0, 10 veya 20. Bireysel satıcıda gönderilse de yok sayılır.
description
hayır
En fazla 5000 karakter.
Varyantsız ürün
Alan
Zorunlu
Açıklama
price
evet
KDV dahil satış fiyatı.
stock
evet
Adet.
compareAtPrice
hayır
Üzeri çizili eski fiyat; satış fiyatından yüksekse indirim görünür.
freeShipping
hayır
Bu üründen kargo bedeli alınmaz. Tutar alanı yoktur: kargo bedeli mağaza ayarından sipariş başına uygulanır. Dijital üründe yok sayılır.
Oluşturulan ürünün fiyatı, stoğu ve durumu sonradan PATCH /products/{id} ile güncellenebilir. Ürün silen bir uç yoktur; satıştan kaldırmak için durumu INACTIVE yapın.
POST
Ürün görseli
POST/products/{id}/images/presign
POST/products/{id}/images
Gerekli izin:products:write
Görsel yükleme iki adımdır. Dosya Shophin sunucusundan geçmez: presign size kısa ömürlü bir yükleme adresi verir, dosyayı doğrudan oraya PUT edersiniz, sonra key değerini bildirirsiniz.
Alan
Zorunlu
Açıklama
fileName
evet
Uzantısıyla birlikte.
contentType
evet
image/jpeg, image/png, image/webp veya image/avif
size
evet
Bayt cinsinden. En fazla 5 MB; imzaya da yazılır, beyandan büyük gövde gönderilemez.
Üçüncü adımda dosya doğrulanır ve dönüştürülür. Beyan ettiğiniz tür ne olursa olsun, açılamayan dosya invalid_request ile reddedilir — doğrulama gerçek baytlar üzerinden yapılır.
İşlem
Kural
Ölçek
Uzun kenar 1600 pikseli aşıyorsa küçültülür. Küçük görsel büyütülmez.
Biçim
Her görsel WebP'ye çevrilir; orijinal silinir.
Boyut
Çıktı 1 MB'ı aşarsa kalite kademeli düşürülür.
Metadata
EXIF temizlenir — telefon fotoğraflarındaki GPS konumu alıcıya gitmez.
Bu yüzden yanıttaki url.webp uzantılıdır ve yüklediğiniz adresle aynı değildir. Kaydedeceğiniz adres yanıttan okunmalıdır.
İlk yüklenen görsel kapak olur: ürün kartlarında ve arama sonucunda gösterilen odur.
POST
Ürünü yayınla
POST/products/{id}/publish
Gerekli izin:products:write
Ürünü DRAFT durumundan ACTIVE durumuna alır. Yayınlanan ürün mağaza sayfanızda görünür ve sepete eklenebilir.
En az bir görsel gerekir. Görselsiz yayınlama denemesi invalid_request ile reddedilir — görselsiz ürün vitrinde boş bir kart olurdu.
İstek
curl -X POST -H "Authorization: Bearer $SHOPHIN_KEY" \
https://api.shophin.com/api/partner/v1/products/cmsn3c6840001s06mm00k9lag/publish
Sepete ekleme Partner API'de değil, herkese açık uçtadır: ürün ekleme satıcının kimliğiyle, sepete ekleme alıcı adına yapılır. İkisini bağlayan alan slug'dır — ürün kimliğiyle değil adresiyle sepete eklenir.
slug başlıktan türetilir ve çakışma olursa sonuna sayı eklenir, yani tahmin edilemez; oluşturma yanıtından okuyun.
Yalnızca gönderdiğiniz alanlar değişir; verilmeyen alana dokunulmaz. Fiyat senkronu yapan entegrasyonlar için: kendi sisteminizde fiyat değiştiğinde Shophin'deki kalem eski fiyatta kalırsa alıcıdan yanlış tutar tahsil edilir.
status: "ACTIVE" yapmak publish ile aynı kurala tabidir: en az bir görsel gerekir. DRAFT kabul edilmez — satıştan kaldırmak için INACTIVE kullanın.
Ürünün kimliğini belirleyen alanlar bu uçtan değiştirilemez: productType, hasVariants, stok kodu ve dijital teslim ayarları. Bunları değiştirmek için ürünü satıcı panelinden düzenleyin.
Varyantlı üründe fiyat ve stok
Varyantlı üründe bu değerler ürün satırında değil varyantta durur; bu uca price veya stock gönderirseniz invalid_request alırsınız. Doğru uç aşağıdaki Varyant güncelle.
PATCH
Varyant güncelle
PATCH/products/{id}/variants/{variantId}
Gerekli izin:products:write
Varyantın fiyat ve stoğunu günceller. variantId değerini ürün oluşturma yanıtındaki variants[].id alanından alırsınız; her varyantın güncel fiyat ve stoğu da aynı dizide döner.
Yanıt ürünün tamamıdır: tek varyantı güncelledikten sonra diğerlerinin güncel hâlini görmek için ikinci bir istek atmanız gerekmez.
Seçenek değerleri (optionValues) değiştirilemez. "M beden"i "L beden"e çevirmek bir güncelleme değil yeni bir varyanttır; sepetlerde ve sipariş geçmişinde o varyant kimliğine bağlı kayıtlar vardır.
Stok senkronunda her varyant için bir istek atılır. Dakikada 600 istek sınırı bu iş için fazlasıyla yeterlidir; yine de tüm kataloğu değil, yalnızca değişenleri gönderin.
Bu uç idempotent değildir. Zaten onaylanmış bir siparişi tekrar onaylarsanız 400 ve invalid_status_transition alırsınız. Yeniden deneme yazarken bu kodu "zaten onaylanmış" olarak ele alın.
Dijital ürün kalemine lisans kodu, indirme bağlantısı ya da serbest metin işler. itemId değerini sipariş detayındaki items[].id alanından alırsınız. Aynı kaleme birden çok teslimat eklenebilir.
Alan
Zorunlu
Açıklama
type
evet
CODE, LINK veya TEXT
content
evet
Kod / bağlantı / metin. LINK için http(s) ile başlamalı.
note
hayır
Alıcıya görünen not.
İstek
curl -X POST -H "Authorization: Bearer $SHOPHIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "CODE",
"content": "LISANS-ABC-123",
"note": "Kurulum sırasında bu kodu girin"
}' \
https://api.shophin.com/api/partner/v1/orders/SPH-MRXLHY0V-C528/items/cmrxlhy0v000dh37gtsibedq6/deliveries
Tüm dijital kalemler teslim edildiyse ve fiziksel kalem yoksa sipariş kendiliğinden DELIVERED olur ve alıcıya teslimat e-postası gider.
Dosya teslimatı bu uçtan yapılamaz; type: "FILE" gönderirseniz invalid_request alırsınız. Dosyayı kendi sunucunuzda barındırıp LINK olarak gönderebilirsiniz.
POST
Fatura gönder
POST/orders/{orderNumber}/invoice
Gerekli izin:invoices:write
Faturayı kime keseceğinizi siparişin invoiceInfo alanından okursunuz. Bu uç ise kestiğiniz faturanın bilgilerini siparişe işler ve alıcıya PDF bağlantılı "faturanız hazır" e-postası gönderir. PDF sizin sunucunuzda kalır; Shophin yalnızca bağlantıyı saklar.
İki ön koşul: sipariş PREPARING ya da sonraki bir durumda olmalı (önce /approve çağırın) ve satıcı kurumsal olmalıdır.
Gövde
Üç alan yeterlidir. Fazladan alan gönderilirse istek invalid_request ile reddedilir.
Alan
Açıklama
number
Fatura numarası, ör. ABC2026000000123
issuedAt
Düzenlenme tarihi (ISO-8601)
pdfUrl
Faturanın PDF adresi. Yalnızca https kabul edilir — bağlantı müşteriye e-posta ile gider.
İdempotenttir. Aynı fatura numarası ikinci kez gönderilirse yeni kayıt açılmaz, mevcut kayıt 200 ile döner ve müşteriye ikinci kez e-posta gitmez. İlk yazımda 201 döner.
Tutar göndermeye gerek yoktur: fatura tutarları ve KDV kırılımı sipariş gövdesinde hazırdır (taxSummary, shippingTax).
Bağlantının kalıcı ve herkese açık olması gerekir — müşteri e-postayı aylar sonra açtığında da faturasına ulaşabilmeli.
Ayrı bir liste ucu yoktur: işlenmiş faturalar sipariş gövdesindeki invoices alanında gelir. Aynı siparişe farklı number ile ikinci bir fatura gönderebilirsiniz; iade faturası bu uçtan gönderilmez.
Genel Bakış
Yeni sipariş için API'yi sürekli yoklamanız gerekmez: olay gerçekleştiğinde Shophin, satıcı panelinde tanımladığınız adrese imzalı bir POST gönderir.
Olay
Ne zaman gönderilir
order.created
Alıcı siparişi verdi; ödeme henüz onaylanmadı. Ödemesi hiç tamamlanmayan siparişler de bu olayı tetikler.
order.paid
Ödeme onaylandı ve sipariş hazırlanmaya uygun. Sevkiyat tetiklemek için bunu dinleyin.
Gövde
data, sipariş nesnesinin alt kümesidir: alan adları ve tipleri GET /orders/{orderNumber} yanıtıyla birebir aynıdır, yalnızca alan sayısı azdır. Aynı modele okuyabilir, iki ayrı yapı tanımlamak zorunda kalmazsınız.
Alan seti bilerek dar: alıcının adı, adresi, iletişim bilgisi ve vergi kırılımı gönderilmez. Ayrıntıyı, olayı aldıktan sonra kendi anahtarınızla çekersiniz. Böylece adres bir kez yanlış yazıldığında kişisel veri yabancı bir sunucuya akmaz.
Yalnızca olaya özgü alanlar en üstte durur: id (olay kimliği), type ve olayın üretildiği an olan createdAt. Siparişin kendi oluşturulma zamanı data.createdAt alanındadır.
Başlıklar
Başlık
Açıklama
X-Shophin-Event
Olay adı (order.paid)
X-Shophin-Event-Id
Olay kimliği. Aynı olay birden çok uca giderse ve yeniden denenirse değişmez — mükerrer işlemeyi bununla eleyin.
X-Shophin-Delivery
Tekil teslim denemesinin kimliği
X-Shophin-Timestamp
Unix saniye; imzaya dahildir
X-Shophin-Signature
v1=<hex> — HMAC-SHA256
İmza Doğrulama
İmzayı mutlaka doğrulayın. Adresiniz internete açıktır; doğrulamayan bir uç, adresi bilen herkesin "şu sipariş ödendi" diyerek sisteminize sahte kayıt düşürmesine izin verir.
İmza, {timestamp}.{ham gövde} metninin gizinizle alınmış HMAC-SHA256 özetidir. İki nokta önemli:
Gövdeyi ham hâliyle imzalayın — JSON'a çevirip yeniden yazdırırsanız boşluk/sıra değişir ve imza tutmaz.
Zaman damgasını da kontrol edin: 5 dakikadan eski isteği reddedin. İmza geçerli olsa bile eski bir istek kaydedilip sonradan tekrar oynatılabilir.
Karşılaştırmayı sabit zamanlı yapın (timingSafeEqual, hash_equals, compare_digest).
import express from "express";
import crypto from "node:crypto";
const SECRET = process.env.SHOPHIN_WEBHOOK_SECRET;
const app = express();
// Ham gövde şart: express.json() gövdeyi yeniden yazdırırsa imza tutmaz
app.post("/shophin/webhook", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-Shophin-Timestamp");
const body = req.body.toString("utf8");
// 1) Tazelik: 5 dakikadan eski istek reddedilir (replay koruması)
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) {
return res.sendStatus(400);
}
// 2) İmza
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${body}`)
.digest("hex");
const provided = (req.get("X-Shophin-Signature") ?? "")
.split(",").map((p) => p.trim())
.find((p) => p.startsWith("v1="))?.slice(3) ?? "";
const a = Buffer.from(expected, "hex");
const b = Buffer.from(provided, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
// 3) Önce 200 dön, işi sonra yap — 10 sn'yi aşan yanıt zaman aşımına düşer
res.sendStatus(200);
const event = JSON.parse(body);
if (islendiMi(event.id)) return; // aynı olay tekrar gelebilir
kuyrugaEkle(event); // ayrıntı: GET /orders/{orderNumber}
});
app.listen(3000);
import hashlib, hmac, os, time
from flask import Flask, request
SECRET = os.environ["SHOPHIN_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/shophin/webhook")
def webhook():
body = request.get_data() # ham gövde
ts = request.headers.get("X-Shophin-Timestamp", "")
sig = request.headers.get("X-Shophin-Signature", "")
# 1) Tazelik
if abs(int(time.time()) - int(ts or 0)) > 300:
return "", 400
# 2) İmza
expected = hmac.new(
SECRET, f"{ts}.".encode() + body, hashlib.sha256
).hexdigest()
provided = next(
(p.strip()[3:] for p in sig.split(",") if p.strip().startswith("v1=")),
"",
)
if not hmac.compare_digest(expected, provided):
return "", 401
# 3) Ağır işi burada yapmayın; kuyruğa bırakıp hemen dönün
event = request.get_json()
if not islendi_mi(event["id"]):
kuyruga_ekle(event) # GET /orders/{orderNumber}
return "", 200
using System.Security.Cryptography;
using System.Text;
app.MapPost("/shophin/webhook", async (HttpRequest req) =>
{
var secret = Environment.GetEnvironmentVariable("SHOPHIN_WEBHOOK_SECRET")!;
using var reader = new StreamReader(req.Body);
var body = await reader.ReadToEndAsync(); // ham gövde
var ts = req.Headers["X-Shophin-Timestamp"].ToString();
var sig = req.Headers["X-Shophin-Signature"].ToString();
// 1) Tazelik
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (!long.TryParse(ts, out var sent) || Math.Abs(now - sent) > 300)
return Results.BadRequest();
// 2) İmza
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(
hmac.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{body}"))).ToLowerInvariant();
var provided = sig.Split(',')
.Select(p => p.Trim())
.FirstOrDefault(p => p.StartsWith("v1="))?[3..] ?? "";
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(provided)))
return Results.Unauthorized();
// 3) Önce yanıt, işi arkada yap
_ = Task.Run(() => Isle(body)); // GET /orders/{orderNumber}
return Results.Ok();
});
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"io"
"math"
"net/http"
"os"
"strconv"
"strings"
"time"
)
var secret = []byte(os.Getenv("SHOPHIN_WEBHOOK_SECRET"))
func webhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body) // ham gövde
ts := r.Header.Get("X-Shophin-Timestamp")
sent, err := strconv.ParseInt(ts, 10, 64)
// 1) Tazelik
if err != nil || math.Abs(float64(time.Now().Unix()-sent)) > 300 {
w.WriteHeader(http.StatusBadRequest)
return
}
// 2) İmza
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
provided := ""
for _, part := range strings.Split(r.Header.Get("X-Shophin-Signature"), ",") {
if part = strings.TrimSpace(part); strings.HasPrefix(part, "v1=") {
provided = part[3:]
}
}
if subtle.ConstantTimeCompare([]byte(expected), []byte(provided)) != 1 {
w.WriteHeader(http.StatusUnauthorized)
return
}
// 3) Önce yanıt, işi arkada yap
w.WriteHeader(http.StatusOK)
var event struct {
ID string `json:"id"`
Type string `json:"type"`
}
json.Unmarshal(body, &event)
if !islendiMi(event.ID) {
go kuyrugaEkle(event) // ayrıntı: GET /orders/{orderNumber}
}
}
func main() {
http.HandleFunc("/shophin/webhook", webhook)
http.ListenAndServe(":3000", nil)
}
Yeniden Deneme ve Hatalar
Sunucunuz 2xx döndürdüğünde teslim başarılı sayılır. Başka her yanıt (ve zaman aşımı) başarısızlıktır ve yeniden denenir.
Zaman aşımı
10 saniye
Deneme sayısı
8 (üstel artışla ≈30 sn → 6 saate yayılır)
Otomatik kapanma
20 teslim üst üste başarısız olursa uç durdurulur; panelden sebebiyle birlikte görünür ve elle yeniden açılır.
Yönlendirme
İzlenmez — 3xx başarısızlık sayılır, son adresi tanımlayın.
Sunucunuz nasıl davranmalı
Önce 200 dönün, sonra işleyin. Ağır işi kuyruğa alın; 10 saniyeyi aşan yanıt zaman aşımına düşer ve aynı olay tekrar gelir.
Mükerrer olaya hazır olun. Yeniden denemede X-Shophin-Event-Id aynı kalır; işlenmiş kimlikleri saklayıp ikinci kez gelirse yok sayın.
Sıra garantisi yoktur.order.paid, order.created'dan önce ulaşabilir; kararınızı gövdedeki status alanına göre verin.
Panelde her ucun son teslim denemeleri, HTTP kodu ve hata mesajıyla listelenir; başarısız bir teslimi düzelttikten sonra oradan yeniden gönderebilirsiniz.
KDV
Fiyatlar KDV dahildir. KDV tutarın üstüne eklenmez, içinden ayrıştırılır. lineTotal üzerine KDV eklerseniz müşteriden tahsil edilenden fazlasını faturalarsınız.
Hesabı sizin yapmanıza gerek yok: her kalemde taxBase ve taxTotal hazır gelir. Kuruş artığı KDV tarafına yazılır, böylece ikisinin toplamı brüt tutara birebir eşittir.
Kargo
Kargo her zaman %20 KDV'lidir ve satıcının tipinden etkilenmez. Tutar shippingTax alanında ayrıca verilir; ücretsiz kargoda null olur.
Oran bazında kırılım
taxSummary, e-Arşiv faturasının istediği oran bazlı satırları verir. Kargo da bu kırılıma dahildir; aynı orandaki ürün kalemleriyle tek satırda toplanır.
Kargoyu ayrı bir fatura satırı olarak göstermek isterseniz shippingTax alanını kullanın ve taxSummary'den çıkarın — iki kez saymayın.
Oranın null gelmesi
vatRate, satıcı bireysel (vergiden muaf) olduğunda null döner ve o kalem taxSummary'ye girmez. Kalemlerden herhangi biri için oran bilinmiyorsa sipariş düzeyindeki taxTotal de null olur — bu durumda faturayı otomatik kesmeyin. Kargo bu kuraldan etkilenmez.
Hem ürün hem kargo oranı sipariş anında dondurulur; geçmiş siparişler sonradan değişmez.
Sipariş Durumları
Durum
Anlamı
PENDING_PAYMENT
Ödeme bekleniyor. İşlem yapmayın.
PAID
Ödeme alındı, satıcı onayı bekleniyor.
PREPARING
Onaylandı. Faturalandırılabilir.
SHIPPED
Kargoya verildi.
DELIVERED
Teslim edildi.
CANCELLED
İptal edildi.
REFUNDED
İade tamamlandı.
API üzerinden yapabileceğiniz geçiş PAID → PREPARING'dir. Dijital-only siparişlerde teslimat sonrası DELIVERED otomatik atanır.
Örnek Akış
Ödenmiş siparişleri çek, onayla, dijital teslimatı yap ve faturayı işle. lisansUret ve faturaKes sizin tarafınızdaki işlevlerdir.
const BASE = "https://api.shophin.com/api/partner/v1";
const KEY = process.env.SHOPHIN_KEY;
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw Object.assign(new Error(data.error.message), data.error);
return data;
}
const { data: orders } = await call("GET", "/orders?status=PAID&limit=100");
for (const order of orders) {
// 1) Onayla — zaten onaylıysa hata sayma
try {
await call("POST", `/orders/${order.orderNumber}/approve`);
} catch (e) {
if (e.code !== "invalid_status_transition") throw e;
}
// 2) Bekleyen dijital kalemlere teslimat işle
for (const item of order.items) {
if (!item.awaitingDelivery) continue;
for (let i = 0; i < item.quantity; i++) {
await call("POST", `/orders/${order.orderNumber}/items/${item.id}/deliveries`, {
type: "CODE",
content: await lisansUret(item.title),
});
}
}
// 3) Faturayı kes ve siparişe işle
const fatura = await faturaKes(order);
await call("POST", `/orders/${order.orderNumber}/invoice`, {
number: fatura.number,
issuedAt: fatura.issuedAt,
pdfUrl: fatura.pdfUrl,
});
}
import os
import requests
BASE = "https://api.shophin.com/api/partner/v1"
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['SHOPHIN_KEY']}",
"Content-Type": "application/json",
})
class ShophinError(Exception):
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def call(method: str, path: str, body: dict | None = None) -> dict:
res = session.request(method, BASE + path, json=body)
data = res.json()
if not res.ok:
raise ShophinError(data["error"]["code"], data["error"]["message"])
return data
orders = call("GET", "/orders?status=PAID&limit=100")["data"]
for order in orders:
no = order["orderNumber"]
# 1) Onayla — zaten onaylıysa hata sayma
try:
call("POST", f"/orders/{no}/approve")
except ShophinError as e:
if e.code != "invalid_status_transition":
raise
# 2) Bekleyen dijital kalemlere teslimat işle
for item in order["items"]:
if not item["awaitingDelivery"]:
continue
for _ in range(item["quantity"]):
call("POST", f"/orders/{no}/items/{item['id']}/deliveries", {
"type": "CODE",
"content": lisans_uret(item["title"]),
})
# 3) Faturayı kes ve siparişe işle
fatura = fatura_kes(order)
call("POST", f"/orders/{no}/invoice", {
"number": fatura["number"],
"issuedAt": fatura["issuedAt"],
"pdfUrl": fatura["pdfUrl"],
})
using System.Net.Http.Headers;
using System.Net.Http.Json;
const string Base = "https://api.shophin.com/api/partner/v1";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("SHOPHIN_KEY"));
// System.Net.Http.Json web varsayılanlarını kullanır: orderNumber ↔ OrderNumber
async Task<T?> Call<T>(HttpMethod method, string path, object? body = null)
{
using var req = new HttpRequestMessage(method, Base + path);
if (body is not null) req.Content = JsonContent.Create(body);
using var res = await http.SendAsync(req);
if (!res.IsSuccessStatusCode)
{
var wrapper = await res.Content.ReadFromJsonAsync<ErrorWrapper>();
throw new ShophinException(wrapper!.Error.Code, wrapper.Error.Message);
}
return await res.Content.ReadFromJsonAsync<T>();
}
var page = await Call<Page>(HttpMethod.Get, "/orders?status=PAID&limit=100");
foreach (var order in page!.Data)
{
// 1) Onayla — zaten onaylıysa hata sayma
try
{
await Call<object>(HttpMethod.Post, $"/orders/{order.OrderNumber}/approve");
}
catch (ShophinException e) when (e.Code == "invalid_status_transition") { }
// 2) Bekleyen dijital kalemlere teslimat işle
foreach (var item in order.Items)
{
if (!item.AwaitingDelivery) continue;
for (var i = 0; i < item.Quantity; i++)
{
await Call<object>(
HttpMethod.Post,
$"/orders/{order.OrderNumber}/items/{item.Id}/deliveries",
new { type = "CODE", content = LisansUret(item.Title) });
}
}
// 3) Faturayı kes ve siparişe işle
var fatura = FaturaKes(order);
await Call<object>(HttpMethod.Post, $"/orders/{order.OrderNumber}/invoice", new
{
number = fatura.Number,
issuedAt = fatura.IssuedAt,
pdfUrl = fatura.PdfUrl,
});
}
record ErrorWrapper(ApiError Error);
record ApiError(string Code, string Message, string? Field);
record Page(List<Order> Data);
record Order(string OrderNumber, List<Item> Items);
record Item(string Id, string Title, int Quantity, bool AwaitingDelivery);
class ShophinException(string code, string message) : Exception(message)
{
public string Code { get; } = code;
}