Örneklerle REST Bağlantısı

Bu sayfa, merchant-provider/v1'in REST bağlantısını istek ve yanıtlarıyla gösterir. Her operasyon için Chargeless'ın gönderdiği isteği, işlem başarılı olduğunda döndürmeniz gereken yanıtı (200), her hata türünde döndürmeniz gereken yanıtı ve Chargeless'ın ardından ne yaptığını bulacaksınız. Başlangıç noktası olarak kullanabileceğiniz eksiksiz bir sunucu sayfanın sonunda yer alır. Her yanıtın arkasındaki kurallar Kendi faturalama sisteminizi bağlamak sayfasında açıklanır; bağlantının kendisi ise sözleşmelerdeki openapi/merchant-provider/v1.yaml dosyasıdır. Bu sayfa ile sözleşme dosyaları arasında bir fark olduğunda dosyalar esas alınır. Adım adım kurulum için Faturalama sisteminizi bir öğleden sonrada bağlayın rehberini izleyin. Aynı operasyonların MCP üzerinden nasıl yapıldığı MCP bağlantısı sayfasındadır.

Bu sayfadaki her JSON gövdesi, Chargeless'ın kendi testlerinde sözleşmenin şemalarına göre doğrulanır ve çoğu, sözleşmedeki örneklerin (schemas/merchant-provider/examples/v1/) kopyasıdır; bu yüzden onları olduğu gibi kendi testlerinize yapıştırabilirsiniz.

Her İstek

Chargeless, temel URL'nize (burada https://billing.example.com; URL /supportroute gibi bir önek içerebilir, bu durumda önek her /v1/… yolunun başına eklenir) şu başlıklarla istek gönderir:

http
POST /v1/refunds HTTP/1.1
Host: billing.example.com
Accept: application/json
Content-Type: application/json
User-Agent: Chargeless (merchant-provider/v1)
Idempotency-Key: 0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6
X-SupportRoute-Timestamp: 1760520000
X-SupportRoute-Signature: v1=1b03ea29388ff9f1dd2bbf894f1e6e359f0fa1003ca246c678ad485249503070
X-SR-Request-Id: 01a1b7d7-f079-7ba8-97a7-851c6d4c1303
X-SR-Correlation-Id: 0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6
X-SR-Channel: ticket

{"transaction_ref":"ch_1001","amount":{"amount_minor":2000,"currency":"USD"},"reason":"service_issue","dry_run":false}
  • Idempotency-Key yalnızca deneme çalıştırması (dry run) olmayan refund ve cancel_subscription isteklerinde gönderilir. Okuma istekleri, get_operation ve dry run istekleri bu başlığı taşımaz.
  • Authorization: Bearer … ve uç (edge) başlıklarınız, panelde tanımladıysanız bunlar da gönderilir.
  • X-SR-* başlıkları loglarınız içindir; paneldeki çağrı günlüğü aynı kimlikleri gösterir. Yardım masası taleplerinden gelen isteklere ayrıca X-SR-External-Ticket başlığı eklenir.
  • Gövdeler JSON biçimindedir ve yalnızca POST isteklerinde gönderilir. Yanıtınızı Content-Type: application/json ile döndürün.

İmzalama, Adım Adım

Her isteği okumadan önce doğrulayın. İmza, v1= önekinin ardından gelen, küçük harfli onaltılık (hex) bir HMAC-SHA256 değeridir. HMAC anahtarı olarak gizli imzalama anahtarınız (signing secret) kullanılır; imzalanan metin, satır sonuyla (\n) birleştirilmiş altı alandan oluşur:

text
{timestamp}\n{METHOD}\n{host}\n{path}\n{idempotency_key}\n{body}

Yukarıdaki iade için, srmp_test_4f7c1e9a2b6d test anahtarıyla imzalanacak metin şu altı satırdır (sonuncusu ham gövdenin bayt bayt aynısıdır):

text
1760520000
POST
billing.example.com
/v1/refunds
0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6
{"transaction_ref":"ch_1001","amount":{"amount_minor":2000,"currency":"USD"},"reason":"service_issue","dry_run":false}

Doğrulayıcınızın hesaplaması gereken başlık değeri de şudur:

text
secret     srmp_test_4f7c1e9a2b6d
timestamp  1760520000
method     POST
host       billing.example.com
path       /v1/refunds
key        0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6
body       {"transaction_ref":"ch_1001","amount":{"amount_minor":2000,"currency":"USD"},"reason":"service_issue","dry_run":false}
signature  v1=1b03ea29388ff9f1dd2bbf894f1e6e359f0fa1003ca246c678ad485249503070

Okuma isteklerinde ne anahtar ne de gövde bulunur; bu yüzden beşinci ve altıncı satırlar boştur. Temel yol (base path) kullanıyorsanız yol, önek dahil, gönderildiği şekliyle imzalanır:

text
secret     srmp_test_4f7c1e9a2b6d
timestamp  1760520005
method     GET
host       billing.example.com
path       /supportroute/v1/transactions/gp.4031-2290
key        (empty)
body       (empty)
signature  v1=d243cf01428686ccff9f852f3f3af7af4fd7b608aa72c8d23bf9dc1ca8c2f70e

Doğrulayıcınızı yayına almadan önce her iki örnekle de test edin. Aynı doğrulama, birkaç satır Node ile:

js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, req, rawBody) {
  const ts = req.headers['x-supportroute-timestamp'] ?? '';
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const msg = [ts, req.method.toUpperCase(), req.headers.host.toLowerCase(), req.url.split('?')[0],
    req.headers['idempotency-key'] ?? '', rawBody.toString('utf8')].join('\n');
  const want = Buffer.from('v1=' + createHmac('sha256', secret).update(msg).digest('hex'));
  const got = Buffer.from(req.headers['x-supportroute-signature'] ?? '');
  return got.length === want.length && timingSafeEqual(got, want);
}

İmza eksik ya da yanlışsa veya zaman damgası sunucunuzun saatinden beş dakikadan fazla sapıyorsa, istek başka hiçbir şeye bakılmadan, hata zarfıyla (error envelope) birlikte 401 ile yanıtlanır:

json
{"error": {"code": "credentials_rejected", "message": "The signature does not verify.", "retryable": false}}

Canlıya geçişteki sözleşme kontrolü iki istek gönderir: biri yanlış bir anahtarla, diğeri on dakika önce imzalanmış olarak. İkisi de 401 (veya 403) almalıdır.

Chargeless Yanıtlarınızı Nasıl Yorumlar

Her hata aynı zarfı kullanır: {"error": {"code", "message", "retryable"}}. code durum koduna karşılık gelir ve retryable onunla tutarlı olmalıdır. message en fazla 500 karakterlik düz metindir ve yalnızca ekibinize gösterilir; hiçbir zaman bir müşteriye veya modele gösterilmez. Bu yüzden içine kart verisi, iletişim bilgisi veya gizli anahtar koymayın.

Durum kodu code retryable Okuma isteğinde İade veya iptal isteğinde
200 (201 ve 202 de aynı şekilde yorumlanır) — — Değer kullanılır succeeded veya pending (yoklanır)
Sözleşmeye uymayan bir gövdeyle 200 — — Okuma başarısız olur; "Sözleşme ihlali" uyarısı açılır Sonuç bilinmiyor: get_operation ile doğrulanır
400 invalid_request false Kesin başarısızlık Kesin: hiçbir şey yeniden denenmez, vaka ekibinize devredilir
401, 403 credentials_rejected false Kesin başarısızlık; "Kimlik bilgileri reddedildi" uyarısı açılır Kesin, okumada olduğu gibi
404 not_found false Nesnenin var olmadığı kabul edilir Koruma reddi sayılır: vaka ekibinize devredilir
409 idempotency_conflict false — Kesin; hiçbir şey yürütülmez, güvenlik olayı olarak kaydedilir
409 in_progress true — Aynı anahtarla yeniden denenir, ardından get_operation ile doğrulanır
422 bir ret kodu false — Ret: işlem reddedilir (durum sınıfı) veya ekibinize devredilir (koruma sınıfı)
429 rate_limited true Yeniden denenir, ardından başarısız sayılır Aynı anahtarla yeniden denenir, ardından get_operation ile doğrulanır
500, 503 internal_error, unavailable true Yeniden denenir, ardından başarısız sayılır Aynı anahtarla yeniden denenir, ardından get_operation ile doğrulanır
zaman aşımı, 3xx veya başka herhangi bir yanıt — — Yeniden denenir (3xx hariç), ardından başarısız sayılır Aynı çağrı içinde asla yeniden denenmez; get_operation ile doğrulanır
  • Yeniden denemeler (retry). Chargeless, bağlantı hatası, 429, 5xx veya 409 in_progress yanıtından sonra tek bir çağrı içinde en fazla üç deneme yapar. Denemeler arasındaki bekleme 200 ms'den 2 sn'ye kadar artar (backoff); idempotency anahtarı aynı kalır, her denemede yeni bir X-SR-Request-Id gönderilir. Retry-After değerine (saniye cinsinden) 2 sn'ye kadar uyulur.
  • Canlı bir konuşma sırasında başarısız okuma. Sohbet, hazır bir mesajla hemen ekibinize devredilir; talep ise 15 dakikaya kadar yeniden denenir, ardından devredilir. Bu tür başarısızlıklar beş veya daha fazlaysa ve son 15 dakikadaki çağrıların en az dörtte birini oluşturuyorsa "Sağlayıcıya ulaşılamıyor" uyarısı açılır.
  • Zaman aşımları. Okumalar, describe ve get_operation için 5 sn; refund ve cancel_subscription için 20 sn.

Her hata sınıfı için kopyalamaya hazır gövdeler:

json
{"error": {"code": "invalid_request", "message": "amount.amount_minor must be at least 1.", "retryable": false}}
json
{"error": {"code": "not_found", "message": "No such transaction.", "retryable": false}}
json
{"error": {"code": "idempotency_conflict", "message": "This key was used with other parameters.", "retryable": false}}
json
{"error": {"code": "in_progress", "message": "The first request with this key is still running.", "retryable": true}}
json
{"error": {"code": "rate_limited", "message": "Too many requests; retry after a second.", "retryable": true}}
json
{"error": {"code": "internal_error", "message": "The billing database did not answer.", "retryable": true}}
json
{"error": {"code": "unavailable", "message": "Down for maintenance until 03:00 UTC.", "retryable": true}}

Retler (422)

Yalnızca refund ve cancel_subscription ret döndürebilir. Ret, o anahtarı hiçbir zaman yürütmeyeceğiniz anlamına gelir: hiç para hareket etmemiştir ve hiçbir zaman etmeyecektir. Her ret kodu bir sınıfa aittir ve sonrasında ne olacağını bu sınıf belirler.

Sınıf Kodlar Ne zaman kullanılır Chargeless ne yapar
Durum (state) already_refunded, amount_exceeds_refundable, not_refundable, subscription_not_active, already_canceled, currency_mismatch Kayıtlarınız, Chargeless'ın kararına dayanak olan olgularla çelişiyor İşlem reddedildi (denied) olarak sonuçlanır; Chargeless hedefi yeniden okur ve müşteriye hazır ret metni gönderilir
Koruma (guard) account_restricted, merchant_hold, kendi tanımladığınız herhangi bir x_ kodu, listede olmayan herhangi bir kod İşlemi kendi yürütme korumanız durduruyor İşlem başarısız (failed) olarak sonuçlanır ve vaka, kodunuz ve mesajınızla birlikte ekibinize devredilir (provider_refused)

Durum sınıfından bir ret:

json
{"error": {"code": "amount_exceeds_refundable", "message": "Only 34.99 USD is left to refund on this charge.", "retryable": false}}

Listedeki kodlardan biriyle ya da kendi x_ kodunuzla koruma sınıfından bir ret:

json
{"error": {"code": "merchant_hold", "message": "Finance hold on this account.", "retryable": false}}
json
{"error": {"code": "x_chargeback_pending", "message": "A chargeback review is open for this customer.", "retryable": false}}

Özel kodlar x_ ile başlar ve ardından [a-z0-9_] karakterlerinden 1 ile 62 arası gelir. Ret, kurallarınızı uygulamanın yeri değildir: belirli bir türdeki isteklerin tamamını reddediyorsanız bu kuralı Chargeless politikanıza yazın; böylece Chargeless size hiç sormaz.

describe

Neyi hangi para birimlerinde uyguladığınızı bildirir. Sözleşme kontrolü sırasında ve bağlantı ayarlarınız değiştiğinde çağrılır; bir müşterinin akışında hiçbir zaman çağrılmaz.

http
GET /v1/describe

200:

json
{
  "contract_version": "1.0",
  "capabilities": {
    "partial_refunds": true,
    "cancel_subscription": true,
    "cancel_at_period_end": true,
    "outcome_callbacks": false
  },
  "currencies": ["USD", "EUR", "JPY", "KWD"],
  "refunds_to_original_payment_method": true
}

Yalnızca tam iade yapan ve aboneliği olmayan bir sistem:

json
{
  "contract_version": "1.0",
  "capabilities": {
    "partial_refunds": false,
    "cancel_subscription": false,
    "cancel_at_period_end": false,
    "outcome_callbacks": false
  },
  "currencies": [
    "TRY"
  ],
  "refunds_to_original_payment_method": true
}
  • contract_version, uyguladığınız revizyondur: "1.0", "1.1" (list_payment_attempts operasyonunu ve payment_attempts yeteneğini ekler) veya "1.2" (isteğe bağlı abonelik alanlarını ekler).
  • Yalnızca desteklediklerinizi bildirin. partial_refunds yoksa Chargeless her zaman yalnızca kalan tutarın tamamını ister; cancel_subscription yoksa get_subscription ve cancel_subscription operasyonlarını hiç çağırmaz.
  • Chargeless, son başarılı sözleşme kontrolünde kaydedilen yetenekleri kullanır; bu yüzden bu yanıtı değiştirdiğinizde kontrolü yeniden çalıştırın.
Yanıtınız Chargeless ne yapar
Yukarıdaki gibi 200 Kontrol, okuma adımlarına geçer
401 / 403 Kontrolün describe satırı başarısız olur; başka hiçbir şey kontrol edilmez
Şemaya uymayan bir gövde describe satırı schema_invalid ile başarısız olur ve hatalı alanı belirtir
5xx, zaman aşımı reachability veya describe satırı başarısız olur

find_customers

Müşterinin yazdıklarından yola çıkarak aday müşterileri daraltır. Bir eşleşme, yazan kişinin kim olduğunu asla kanıtlamaz: Chargeless kimliği ayrıca doğrular (imzalı bir token, e-posta kodu veya e-posta bağlantısıyla) ve her tanımlayıcıyı, güncel olarak okuduğu müşteri kaydı üzerinde kendisi yeniden karşılaştırır.

http
POST /v1/customers/search
json
{"email": "[email protected]"}

Ya da email, phone (E.164), customer_ref, transaction_ref, order_ref ve card_last4 alanlarının herhangi bir birleşimi gönderilebilir; yalnızca verilen tanımlayıcıların tümüne uyan müşterileri döndürün:

json
{"order_ref": "ord_5521", "card_last4": "4242"}

200, en fazla 10 aday:

json
{"candidates": [{"customer_ref": "cus_7Hk2Lm"}]}

Eşleşme yoksa da yanıt 200'dür, asla 404 değildir:

json
{"candidates": []}
Yanıtınız Ne zaman Chargeless ne yapar
Adaylarla birlikte 200 Bir veya birden fazla müşteri eşleşiyor Her adayı kullanmadan önce get_customer ile okur
[] ile 200 Hiçbir müşteri eşleşmiyor Eşleşme yok olarak değerlendirir
400 invalid_request Sorguda hiç tanımlayıcı yok ya da okuyamadığınız bir tanımlayıcı var O arama için kesin başarısızlık
404 Hiçbir zaman Buradaki 404, "eşleşme yok" olarak değil, başarısız bir çağrı olarak yorumlanır

E-posta adresini, kayıtlı her adresle büyük/küçük harf ayrımı yapmadan karşılaştırın.

get_customer

Politikanızın okuyabileceği bilgilerle birlikte müşteri kaydı: son 365 günün iadeleri ve ters ibrazları. Kimlik doğrulama sırasında ve her kararda çağrılır.

http
GET /v1/customers/cus_7Hk2Lm

200:

json
{
  "customer_ref": "cus_7Hk2Lm",
  "emails": ["[email protected]", "[email protected]"],
  "phones": ["+905551234567"],
  "account_status": "active",
  "account_created_at": "2025-03-02T10:15:00Z",
  "prior_refunds": [
    {
      "at": "2026-09-30T08:01:12Z",
      "amount": {"amount_minor": 1999, "currency": "USD"},
      "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:chat:0199d0aa-1111-7222-8333-944455566677:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
      "reversed_at": null
    },
    {
      "at": "2026-04-11T16:20:00Z",
      "amount": {"amount_minor": 500, "currency": "USD"},
      "idempotency_key": null,
      "reversed_at": null
    }
  ],
  "prior_chargebacks": [
    {"at": "2026-02-01T09:00:00Z", "amount": {"amount_minor": 4999, "currency": "USD"}, "status": "won"}
  ],
  "attributes": {"credits_used": 0, "segment": "self_serve", "beta": true}
}
  • Chargeless'ın yaptığı bir iade, kendi anahtarını bayt bayt aynı haliyle taşır; başka bir yolla yapılan iadede bu alan null olur. Chargeless kendi iadelerini bu sayede yalnızca bir kez sayar.
  • emails ve phones yalnızca kimlik doğrulama mesajlarını kayıtlı bir adrese göndermek için kullanılır. Hiçbir zaman bir modele veya müşteriye gösterilmez, panelde de yer almaz.
  • attributes (isteğe bağlı, en fazla 20 adet) politikanızın kurallarında okunabilir. İçine iletişim veya kart verisi koymayın.
Yanıtınız Ne zaman Chargeless ne yapar
200 Müşteri var Kimlik doğrulama veya karar için kullanır
404 not_found Böyle bir müşteri yok Müşterinin var olmadığı kabul edilir: ona hiçbir kimlik doğrulaması bağlanmaz ve onun hakkında hiçbir karar verilmez
5xx, 429, zaman aşımı Şu anda yanıt veremiyorsunuz Yeniden denenir, ardından başarısız okuma sayılır (yukarıdaki tabloya bakın)

list_transactions

Müşterinin son ödemeleri ve abonelikleri. Doğrulanmış bir müşterinin ödemelerine her bakıldığında çağrılır.

http
GET /v1/customers/cus_7Hk2Lm/transactions

200; ödemelerden ve aboneliklerden en fazla 50'şer adet, tümü istenen müşteriye ait:

json
{
  "transactions": [
    {
      "transaction_ref": "gp.4031-2290",
      "customer_ref": "cus_7Hk2Lm",
      "order_ref": "ord_5521",
      "subscription_ref": null,
      "state": "succeeded",
      "amount": {
        "amount_minor": 1200,
        "currency": "JPY"
      },
      "created_at": "2026-10-10T01:02:03Z",
      "description": null,
      "product_type": null,
      "charge_type": "top_up",
      "processor": "google_play",
      "refund_route": "unsupported",
      "card_last4": null,
      "card_brand": null,
      "disputed": false,
      "refunds": [],
      "settlement": {
        "amount": {
          "amount_minor": 739,
          "currency": "EUR"
        }
      }
    },
    {
      "transaction_ref": "ch_1001",
      "customer_ref": "cus_7Hk2Lm",
      "order_ref": null,
      "subscription_ref": "sub_88",
      "state": "succeeded",
      "amount": {
        "amount_minor": 4999,
        "currency": "USD"
      },
      "created_at": "2026-10-01T06:00:03Z",
      "description": "Pro plan, monthly",
      "product_type": "pro_plan",
      "charge_type": "renewal",
      "processor": "adyen",
      "refund_route": "provider",
      "card_last4": "4242",
      "card_brand": "visa",
      "disputed": false,
      "refunds": [
        {
          "at": "2026-10-02T11:00:00Z",
          "amount": {
            "amount_minor": 1000,
            "currency": "USD"
          },
          "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:chat:0199d0aa-1111-7222-8333-944455566677:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
          "reversed_at": "2026-10-05T09:30:00Z"
        },
        {
          "at": "2026-10-03T12:00:00Z",
          "amount": {
            "amount_minor": 500,
            "currency": "USD"
          },
          "idempotency_key": null,
          "reversed_at": null
        }
      ],
      "settlement": null,
      "attributes": {
        "seats": 3
      }
    }
  ],
  "subscriptions": [
    {
      "subscription_ref": "sub_88",
      "customer_ref": "cus_7Hk2Lm",
      "phase": "active",
      "cancel_at_period_end": false,
      "current_period_end": "2026-11-01T06:00:00Z",
      "trial_end": "2026-05-15T06:00:00Z",
      "billing_interval": {
        "unit": "month",
        "count": 1
      },
      "plan_ref": "pro_monthly",
      "renewal_count": 5,
      "created_at": "2026-05-01T06:00:00Z",
      "last_charge": {
        "transaction_ref": "ch_1001",
        "at": "2026-10-01T06:00:03Z",
        "amount": {
          "amount_minor": 4999,
          "currency": "USD"
        }
      },
      "attributes": {
        "seats": 3
      }
    }
  ]
}

Ödeme ve abonelik kayıtlarındaki her alan her zaman bulunur; değeri bilinmeyen alanlar null olur. Henüz hiçbir kaydı olmayan bir müşteri:

json
{"transactions": [], "subscriptions": []}
Yanıtınız Ne zaman Chargeless ne yapar
200 Müşteri var, listelenecek bir şey olmasa bile Kimliği doğrulandıktan sonra bunları müşteriye gösterir
404 not_found Böyle bir müşteri yok Müşterinin var olmadığı kabul edilir
Başka bir müşteriye ait kayıt içeren bir liste Hiçbir zaman Yanıtın tamamı sözleşme ihlali sayılır

get_transaction

Tek bir ödeme; bu ödemeyle ilgili her karardan önce güncel olarak okunur: iade süresi created_at'ten itibaren işler, iade edilebilir kalan ise amount ve refunds alanlarından hesaplanır.

http
GET /v1/transactions/ch_1001

200, gerçekleşmiş tüm iadelerle birlikte:

json
{
  "transaction_ref": "ch_1001",
  "customer_ref": "cus_7Hk2Lm",
  "order_ref": null,
  "subscription_ref": "sub_88",
  "state": "succeeded",
  "amount": {"amount_minor": 4999, "currency": "USD"},
  "created_at": "2026-10-01T06:00:03Z",
  "description": "Pro plan, monthly",
  "product_type": "pro_plan",
  "charge_type": "renewal",
  "processor": "adyen",
  "refund_route": "provider",
  "card_last4": "4242",
  "card_brand": "visa",
  "disputed": false,
  "refunds": [
    {
      "at": "2026-10-02T11:00:00Z",
      "amount": {"amount_minor": 1000, "currency": "USD"},
      "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:chat:0199d0aa-1111-7222-8333-944455566677:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
      "reversed_at": "2026-10-05T09:30:00Z"
    },
    {
      "at": "2026-10-03T12:00:00Z",
      "amount": {"amount_minor": 500, "currency": "USD"},
      "idempotency_key": null,
      "reversed_at": null
    }
  ],
  "settlement": null,
  "attributes": {"seats": 3}
}
  • Geri alınmış (reversed) bir iade, reversed_at ile listede kalır; bekleyen bir iade ise gerçekleşene kadar listelenmez.
  • customer_ref ödemenin güncel sahibidir: Chargeless yalnızca bu alan kimliği doğrulanmış müşteriyi gösteriyorsa işlem yapar.
  • Tutarlar, para biriminin en küçük birimi cinsinden tam sayıdır (JPY için 0 basamak, KWD için 3, diğerlerinin çoğu için 2): 4999 geçerlidir; 49.99, 4999.0 ve "4999" geçerli değildir.
  • Referanslar tek bir kurala uyar: harf, rakam, ., _, : ve - karakterlerinden oluşan 1 ile 128 karakter. Kanonik bir UUID dışında hiçbir zaman 13 veya daha fazla ardışık rakam içermez (böyle bir dizi kart numarasıyla karışabilir) (Referanslar).
Yanıtınız Ne zaman Chargeless ne yapar
200 Ödeme var Son 60 saniye içinde okunan bu olgulara göre karar verir
404 not_found Böyle bir ödeme yok Ödemenin var olmadığı kabul edilir: hakkında hiçbir karar verilmez
Şemaya uymayan bir gövde Bir alan eksik veya türü yanlış Okuma başarısız olur; sözleşme kontrolü hatalı alanı belirtir, örneğin `amount.amount_minor` is missing
5xx, 429, zaman aşımı Şu anda yanıt veremiyorsunuz Yeniden denenir, ardından başarısız okuma sayılır: güncel bir okuma olmadan karar verilmez

get_subscription

Tek bir abonelik; bu abonelikle ilgili her karardan önce güncel olarak okunur. Yalnızca cancel_subscription yeteneği bildirildiyse çağrılır.

http
GET /v1/subscriptions/sub_88

200:

json
{
  "subscription_ref": "sub_88",
  "customer_ref": "cus_7Hk2Lm",
  "phase": "active",
  "cancel_at_period_end": false,
  "current_period_end": "2026-11-01T06:00:00Z",
  "trial_end": "2026-05-15T06:00:00Z",
  "billing_interval": {
    "unit": "month",
    "count": 1
  },
  "plan_ref": "pro_monthly",
  "renewal_count": 5,
  "created_at": "2026-05-01T06:00:00Z",
  "last_charge": {
    "transaction_ref": "ch_1001",
    "at": "2026-10-01T06:00:03Z",
    "amount": {
      "amount_minor": 4999,
      "currency": "USD"
    }
  },
  "attributes": {
    "seats": 3
  }
}

Revizyon 1.2, isteğe bağlı canceled_at, ended_at, price ve plan_name alanlarını ekler; bu alanlar panelde gösterilir ve hiçbir kararda kullanılmaz:

json
{
  "subscription_ref": "sub_93",
  "customer_ref": "cus_7Hk2Lm",
  "phase": "canceled",
  "cancel_at_period_end": false,
  "current_period_end": null,
  "trial_end": null,
  "billing_interval": {"unit": "year", "count": 1},
  "plan_ref": "pro_yearly",
  "renewal_count": 1,
  "created_at": "2024-09-01T06:00:00Z",
  "last_charge": {"transaction_ref": "ch_0907", "at": "2025-09-01T06:00:02Z", "amount": {"amount_minor": 49900, "currency": "EUR"}},
  "canceled_at": "2026-03-14T09:12:00Z",
  "ended_at": "2026-03-14T09:12:00Z",
  "price": {"amount_minor": 49900, "currency": "EUR"},
  "plan_name": "Pro (yearly)"
}

phase şu değerlerden birini alır: trial, active, grace, past_due, paused, canceled veya expired; son ikisi nihai durumdur. Hatalar get_transaction ile aynıdır.

refund

Yalnızca Chargeless politikasının onaylayıp kaydettiği bir iade için, iadenin idempotency anahtarıyla çağrılır. İadeler her zaman orijinal ödeme yöntemine yapılır; Chargeless hiçbir zaman bir iade hedefi göndermez.

http
POST /v1/refunds
Idempotency-Key: 0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6
json
{"transaction_ref": "ch_1001", "amount": {"amount_minor": 2000, "currency": "USD"}, "reason": "service_issue", "dry_run": false}

İade tamamlandığında 200:

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "succeeded",
  "transaction_ref": "ch_1001",
  "amount": {"amount_minor": 2000, "currency": "USD"},
  "refund_ref": "re_77",
  "processed_at": "2026-10-15T09:12:44Z"
}

İşlemciniz (processor) iadeyi daha sonra onaylayacaksa da 200; bu durumda Chargeless, çağrıdan 30 sn, 2 dk, 10 dk ve 30 dk sonra, ardından 24 saate kadar saatte bir get_operation ile durumu sorgular:

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "pending",
  "transaction_ref": "ch_1001",
  "amount": {"amount_minor": 2000, "currency": "USD"},
  "refund_ref": null
}

Sonuç; anahtarı, ödemeyi ve tutarı birebir yansıtır. Bunları birebir yansıtmayan bir yanıtın sonucu bilinmiyor sayılır ve get_operation ile doğrulanır.

Bir dry run isteği anahtar taşımaz; eksiksiz doğrulanır, ancak hiçbir şeyi değiştirmez ve hiçbir şey saklamaz. Dry run isteğini yalnızca sözleşme kontrolü ve uyumluluk kiti gönderir; bir müşterinin vakası için asla gönderilmez:

json
{"transaction_ref": "ch_1001", "amount": {"amount_minor": 3499, "currency": "USD"}, "reason": "duplicate", "dry_run": true}
json
{"idempotency_key": null, "status": "would_succeed", "transaction_ref": "ch_1001", "amount": {"amount_minor": 3499, "currency": "USD"}, "refund_ref": null}
Yanıtınız Ne zaman Chargeless ne yapar
200 succeeded İade gerçekleşti İşlem yürütülmüş sayılır; müşteri bir şablon üzerinden bilgilendirilir
200 pending İşlemciniz daha sonra onaylayacak Sonuçlanana kadar, en fazla 24 saat boyunca get_operation ile yoklar; ardından vakayı onaylanmamış olarak ekibinize devreder
Durum sınıfı koduyla 422 already_refunded, amount_exceeds_refundable, not_refundable, currency_mismatch Reddedilir; müşteriye hazır ret metni gönderilir
Koruma sınıfı koduyla 422 account_restricted, merchant_hold, x_… Başarısız olur; vaka, kodunuz ve mesajınızla birlikte ekibinize devredilir
404 not_found Böyle bir ödeme yok Koruma reddi sayılır: vaka ekibinize devredilir
409 idempotency_conflict Bu anahtar daha önce başka bir tutar veya ödemeyle geldi Kesin; hiçbir şey yürütülmez, bir güvenlik olayı kaydedilir ve "Sözleşme ihlali" uyarısı açılır
409 in_progress Anahtarın ilk çağrısı hâlâ sürüyor Aynı anahtarla yeniden denenir, ardından get_operation ile doğrulanır
400 invalid_request Okuyamadığınız bir gövde ya da anahtarsız gerçek bir iade Kesin: vaka ekibinize devredilir
401, 403 İmzayı veya kimlik bilgilerini reddettiniz İlk denemede kesin; sonraki bir denemede gelen 401 sonucu belirsiz bırakır
429, 5xx, zaman aşımı Şu anda yanıt veremiyorsunuz Aynı anahtarla yeniden denenir (zaman aşımından sonra denenmez), ardından get_operation ile doğrulanır

Aynı anahtar yeniden geldiğinde:

Yeniden gönderildiğinde Yanıt
aynı parametrelerle Yeniden iade yapılmadan ilk sonuç (200); bu arada pending sonuç succeeded olmuş olabilir
farklı parametrelerle (bir tutar, bir ödeme) 409 idempotency_conflict, hiçbir şey yürütülmez
ilk çağrı hâlâ sürerken 409 in_progress
bir retten sonra Aynı ret (422)

Daha önce yürüttüğünüz bir anahtara asla 401 ya da başka bir yanıt vermeyin: ilk sonucu yeniden döndürün (replay). Kendi kayıtlarınızdaki iade edilebilir tutarı aşan bir iadeyi, Chargeless önceden kontrol etmiş olsa bile amount_exceeds_refundable ile reddedin: bu arada ödeme başka bir yoldan iade edilmiş olabilir.

cancel_subscription

Yalnızca Chargeless politikasının onayladığı bir iptal için, iptalin anahtarıyla çağrılır. Yalnızca cancel_subscription yeteneği bildirildiyse çağrılır; at_period_end: true ise yalnızca cancel_at_period_end yeteneğini bildirdiyseniz gönderilir.

http
POST /v1/subscriptions/sub_88/cancel
Idempotency-Key: 0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:cancel_subscription:0199e2c4-8888-7c8d-9e0f-a1b2c3d4e5f6
json
{"at_period_end": true, "reason": "forgot_to_cancel", "dry_run": false}

200:

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:cancel_subscription:0199e2c4-8888-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "succeeded",
  "subscription_ref": "sub_88",
  "at_period_end": true,
  "effective_at": "2026-11-01T06:00:00Z"
}

Dry run:

json
{"idempotency_key": null, "status": "would_succeed", "subscription_ref": "sub_88", "at_period_end": false, "effective_at": null}

Zaten iptal edilmiş bir abonelik:

json
{"error": {"code": "already_canceled", "message": "Canceled on 2026-10-02.", "retryable": false}}
Yanıtınız Ne zaman Chargeless ne yapar
200 succeeded İptal edildi (veya dönem sonuna planlandı) Yürütülür; müşteri bir şablon üzerinden bilgilendirilir
200 pending Sisteminiz daha sonra onaylayacak İadede olduğu gibi get_operation ile yoklar
422 already_canceled, subscription_not_active Kayıtlarınız kararla çelişiyor Reddedilir; müşteriye hazır ret metni gönderilir
Koruma sınıfı koduyla 422, 404 İşlemi korumanız durduruyor veya böyle bir abonelik yok Başarısız olur; vaka ekibinize devredilir
409, 400, 401, 429, 5xx, zaman aşımı refund ile aynı refund ile aynı

get_operation

Chargeless'ın gönderdiği bir anahtarın durumunu döndürür. Açık kalmış bir sonucu (bir pending, bir zaman aşımı, okunamayan bir yanıt) doğrulamak için ve bir sonuç bildiriminden (outcome callback) sonra sorgulanır. Hiçbir zaman 404 döndürmez.

http
GET /v1/operations/0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6

Gerçekleşmiş bir iade için 200:

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "succeeded",
  "operation": "refund",
  "target_ref": "ch_1001",
  "result": {
    "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
    "status": "succeeded",
    "transaction_ref": "ch_1001",
    "amount": {"amount_minor": 2000, "currency": "USD"},
    "refund_ref": "re_77",
    "processed_at": "2026-10-15T09:12:44Z"
  },
  "updated_at": "2026-10-15T09:14:02Z"
}

Hâlâ beklemede:

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "pending",
  "operation": "refund",
  "target_ref": "ch_1001",
  "result": {
    "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
    "status": "pending",
    "transaction_ref": "ch_1001",
    "amount": {"amount_minor": 2000, "currency": "USD"},
    "refund_ref": null
  },
  "updated_at": "2026-10-15T09:12:44Z"
}

Reddedilmiş (422 yanıtındaki retle aynı):

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "failed",
  "operation": "refund",
  "target_ref": "ch_1001",
  "refusal": {"code": "amount_exceeds_refundable", "message": "Only 34.99 USD is left to refund on this charge."},
  "updated_at": "2026-10-15T09:12:44Z"
}

Başarıyla sonuçlandıktan sonra geri aldığınız bir iade (reversal; succeeded durumundan sonra izin verilen tek değişiklik budur):

json
{
  "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
  "status": "failed",
  "operation": "refund",
  "target_ref": "ch_1001",
  "result": {
    "idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6",
    "status": "succeeded",
    "transaction_ref": "ch_1001",
    "amount": {"amount_minor": 2000, "currency": "USD"},
    "refund_ref": "re_77",
    "processed_at": "2026-10-15T09:12:44Z"
  },
  "reversed_at": "2026-10-18T10:00:00Z",
  "updated_at": "2026-10-18T10:00:00Z"
}

Hiç almadığınız bir anahtar için, ve yalnızca bu durumda:

json
{"idempotency_key": "0199a3c1-0a0b-7c0d-8e0f-101112131415:providercheck:never-used", "status": "unknown"}
Yanıtınız Chargeless ne yapar
succeeded veya failed Anahtar için nihai sonuçtur; yoklama sona erer
pending Yoklama takvimine göre yeniden sorar
24 saat içinde unknown İşlemi aynı anahtarla yeniden gönderir (anahtarı hiç almadığınız için güvenlidir); arada acil durdurma devreye girdiyse veya bağlantı ayarlarınız değiştiyse göndermez
failed'dan sonra succeeded ya da herhangi bir yanıttan sonra unknown Sözleşme ihlalidir: yok sayılır; iade yine de ödemeden düşülmüş sayılır
404 Sözleşme ihlalidir; bu yanıtı asla vermeyin

Her anahtarı en az 30 gün saklayın.

list_payment_attempts

Yalnızca payment_attempts yeteneği bildirildiyse çağrılır (revizyon 1.1). Canlıya geçtikten sonra ödeme içgörüleri sayfasını doldurur; hiçbir iadeye veya iptale veri sağlamaz. Sorgu, imzalanan gövdenin kendisidir:

http
POST /v1/payment-attempts/list
json
{"updated_since": "2025-09-20T00:00:00Z", "limit": 200}

Yanıt, (updated_at, attempt_ref) sırasına göre dizilmiş bir deneme sayfasıdır; has_more alanını ve daha fazla kayıt varsa cursor olarak geri gönderilecek next_cursor değerini içerir. Son sayfa örneği:

json
{
  "attempts": [],
  "next_cursor": null,
  "has_more": false
}
Yanıtınız Ne zaman Chargeless ne yapar
200 Yetenek bildirildiyse her zaman, boş sayfa dahil Denemeleri saklar (BIN kısaltılarak; kart numarası asla saklanmaz)
İmleç içeren bir istekte 400 invalid_request İmlecin süresi dolmuş veya imleç bilinmiyor İmleç olmadan, kendi başlangıç sınırından yeniden okur
404 Yeteneği bildirmiyorsunuz Bu durumda hiç çağrılmaz
Retry-After ile 429 Chargeless'ın yavaşlamasını istiyorsunuz Bekler; sayfalar art arda, dakikada en fazla 10 sayfa okunur

Alanlar ve kurallar (kart numarası asla yer almaz) Ödeme denemeleri bölümünde açıklanır.

Eksiksiz Bir Sunucu

Aşağıdaki sunucu REST bağlantısını hiçbir paket kullanmadan, düz Node ile uygular: bellekte tutulan bir müşteri, bir ödeme ve bir abonelik, faturalama sisteminizin yerine geçer. Sunucu her isteğin imzasını ham baytlar üzerinden doğrular, idempotency'yi ve kesinliği (finality) her anahtar için korur, dry run isteklerine hiçbir şeyi değiştirmeden yanıt verir, kalanı aşan iadeleri reddeder ve providercheck -sandbox'ın her satırını geçer (providercheck/2 ile kontrol edildi). Bellekteki depoyu faturalama sisteminize yapılan çağrılarla değiştirin; kuralları aynen koruyun.

sh
SIGNING_SECRET=srmps_… node server.mjs     # serves http://localhost:8080
js
// A minimal merchant-provider/v1 server, REST binding, in plain Node (18 or later, no packages).
// One customer, one charge and one subscription kept in memory stand in for your billing system.
//   SIGNING_SECRET=srmps_… node server.mjs
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET = process.env.SIGNING_SECRET; // srmps_… on your sandbox, srmp_… in production
const PORT = Number(process.env.PORT ?? 8080);
if (!SECRET) throw new Error('set SIGNING_SECRET');

const now = () => new Date().toISOString();
const days = (n) => new Date(Date.now() + n * 86_400_000).toISOString();
const usd = (minor) => ({ amount_minor: minor, currency: 'USD' });

// --- Your system of record ----------------------------------------------------------------------
const customers = {
  cus_1001: { customer_ref: 'cus_1001', emails: ['[email protected]'], phones: [],
    account_status: 'active', account_created_at: '2025-03-02T10:15:00Z' },
};
const transactions = {
  ch_1001: { transaction_ref: 'ch_1001', customer_ref: 'cus_1001', order_ref: null,
    subscription_ref: 'sub_1001', state: 'succeeded', amount: usd(4999), created_at: days(-3),
    description: 'Pro plan, monthly', product_type: null, charge_type: 'renewal',
    processor: 'in_house', refund_route: 'provider', card_last4: '4242', card_brand: 'visa',
    disputed: false, refunds: [], settlement: null },
};
const subscriptions = {
  sub_1001: { subscription_ref: 'sub_1001', customer_ref: 'cus_1001', phase: 'active',
    cancel_at_period_end: false, current_period_end: days(27), trial_end: null,
    billing_interval: { unit: 'month', count: 1 }, plan_ref: 'pro_monthly', renewal_count: 3,
    created_at: days(-93), last_charge: { transaction_ref: 'ch_1001', at: days(-3), amount: usd(4999) } },
};
// Every key Chargeless sent, kept at least 30 days: key → { fingerprint, operation }.
const operations = new Map();

const ownRefunds = (ref) => Object.values(transactions).filter((t) => t.customer_ref === ref)
  .flatMap((t) => t.refunds).sort((a, b) => b.at.localeCompare(a.at)).slice(0, 50);
const customerView = (c) => ({ ...c, prior_refunds: ownRefunds(c.customer_ref), prior_chargebacks: [] });
const refundable = (t) => t.amount.amount_minor -
  t.refunds.filter((r) => r.reversed_at === null).reduce((sum, r) => sum + r.amount.amount_minor, 0);

// --- The signature (section 3) ------------------------------------------------------------------
function verified(req, body) {
  const ts = String(req.headers['x-supportroute-timestamp'] ?? '');
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const head = [ts, req.method, String(req.headers.host ?? '').toLowerCase(), req.url.split('?')[0],
    String(req.headers['idempotency-key'] ?? '')].join('\n') + '\n';
  const want = Buffer.from('v1=' + createHmac('sha256', SECRET).update(head).update(body).digest('hex'));
  const got = Buffer.from(String(req.headers['x-supportroute-signature'] ?? ''));
  return got.length === want.length && timingSafeEqual(got, want);
}

// --- Answers ------------------------------------------------------------------------------------
const ok = (body) => [200, body];
function fail(status, code, message) {
  const retryable = status === 429 || status >= 500 || (status === 409 && code === 'in_progress');
  return [status, { error: { code, message, retryable } }];
}
const notFound = () => fail(404, 'not_found', 'no such object');
const invalid = (message) => fail(400, 'invalid_request', message);

// A mutation's key rule (section 4), and a replay: the first answer again, or a conflict.
function keyRule(key, dryRun) {
  if (dryRun && key) return invalid('a dry run carries no Idempotency-Key');
  if (!dryRun && !key) return invalid('Idempotency-Key is required');
  return null;
}
function replay(key, fingerprint) {
  const { fingerprint: first, operation } = operations.get(key);
  if (first !== fingerprint) return fail(409, 'idempotency_conflict', 'this key was used with other parameters');
  if (operation.status === 'failed') return fail(422, operation.refusal.code, operation.refusal.message);
  return ok(operation.result);
}
function refuse(key, dryRun, fingerprint, kind, target, code, message) {
  if (!dryRun) operations.set(key, { fingerprint, operation: { idempotency_key: key, status: 'failed',
    operation: kind, target_ref: target, refusal: { code, message }, updated_at: now() } });
  return fail(422, code, message);
}

// --- The operations (section 2) -----------------------------------------------------------------
function describe() {
  return ok({ contract_version: '1.0', currencies: ['USD'], refunds_to_original_payment_method: true,
    capabilities: { partial_refunds: true, cancel_subscription: true, cancel_at_period_end: true,
      outcome_callbacks: false } });
}

function findCustomers(q) {
  const tests = {
    email: (c, v) => c.emails.some((e) => e.toLowerCase() === String(v).toLowerCase()),
    phone: (c, v) => c.phones.includes(v),
    customer_ref: (c, v) => c.customer_ref === v,
    transaction_ref: (c, v) => transactions[v]?.customer_ref === c.customer_ref,
    order_ref: (c, v) => Object.values(transactions).some((t) => t.order_ref === v && t.customer_ref === c.customer_ref),
    card_last4: (c, v) => Object.values(transactions).some((t) => t.card_last4 === v && t.customer_ref === c.customer_ref),
  };
  const given = Object.entries(q ?? {});
  if (given.length === 0 || given.some(([k]) => !(k in tests))) return invalid('give one or more known identifiers');
  const hits = Object.values(customers).filter((c) => given.every(([k, v]) => tests[k](c, v)));
  return ok({ candidates: hits.slice(0, 10).map((c) => ({ customer_ref: c.customer_ref })) });
}

function refund(key, r) {
  const amount = r?.amount;
  if (typeof r?.transaction_ref !== 'string' || !Number.isInteger(amount?.amount_minor) ||
      amount.amount_minor < 1 || typeof amount.currency !== 'string' || typeof r.reason !== 'string' ||
      typeof r.dry_run !== 'boolean') return invalid('the body does not match refund-request.json');
  const bad = keyRule(key, r.dry_run);
  if (bad) return bad;
  const fingerprint = JSON.stringify(['refund', r.transaction_ref, amount.amount_minor, amount.currency, r.reason]);
  if (!r.dry_run && operations.has(key)) return replay(key, fingerprint);
  const t = transactions[r.transaction_ref];
  if (!t) return notFound();
  const refusal = amount.currency !== t.amount.currency ? ['currency_mismatch', 'the charge is in another currency']
    : t.state !== 'succeeded' || t.disputed ? ['not_refundable', 'this charge cannot be refunded']
    : amount.amount_minor > refundable(t) ? ['amount_exceeds_refundable', `only ${refundable(t)} minor units are left to refund`]
    : null;
  if (refusal) return refuse(key, r.dry_run, fingerprint, 'refund', t.transaction_ref, ...refusal);
  if (r.dry_run) return ok({ idempotency_key: null, status: 'would_succeed',
    transaction_ref: t.transaction_ref, amount, refund_ref: null });
  // Execute once, at your processor, to the original payment method; then record it under the key.
  const at = now();
  t.refunds.push({ at, amount, idempotency_key: key, reversed_at: null });
  const result = { idempotency_key: key, status: 'succeeded', transaction_ref: t.transaction_ref,
    amount, refund_ref: `re_${operations.size + 1}`, processed_at: at };
  operations.set(key, { fingerprint, operation: { idempotency_key: key, status: 'succeeded',
    operation: 'refund', target_ref: t.transaction_ref, result, updated_at: at } });
  return ok(result);
}

function cancel(key, ref, r) {
  const s = subscriptions[ref];
  if (typeof r?.at_period_end !== 'boolean' || typeof r.reason !== 'string' || typeof r.dry_run !== 'boolean')
    return invalid('the body does not match cancel-request.json');
  const bad = keyRule(key, r.dry_run);
  if (bad) return bad;
  const fingerprint = JSON.stringify(['cancel_subscription', ref, r.at_period_end, r.reason]);
  if (!r.dry_run && operations.has(key)) return replay(key, fingerprint);
  if (!s) return notFound();
  if (s.phase === 'canceled' || s.phase === 'expired' || (r.at_period_end && s.cancel_at_period_end))
    return refuse(key, r.dry_run, fingerprint, 'cancel_subscription', ref, 'already_canceled', 'already canceled');
  if (r.dry_run) return ok({ idempotency_key: null, status: 'would_succeed', subscription_ref: ref,
    at_period_end: r.at_period_end, effective_at: null });
  const at = now();
  let effective = at;
  if (r.at_period_end) {
    s.cancel_at_period_end = true;
    effective = s.current_period_end;
  } else {
    Object.assign(s, { phase: 'canceled', cancel_at_period_end: false, current_period_end: null });
  }
  const result = { idempotency_key: key, status: 'succeeded', subscription_ref: ref,
    at_period_end: r.at_period_end, effective_at: effective };
  operations.set(key, { fingerprint, operation: { idempotency_key: key, status: 'succeeded',
    operation: 'cancel_subscription', target_ref: ref, result, updated_at: at } });
  return ok(result);
}

function getOperation(key) {
  return ok(operations.get(key)?.operation ?? { idempotency_key: key, status: 'unknown' });
}

// --- Routing ------------------------------------------------------------------------------------
function route(method, path, key, body) {
  let m;
  if (method === 'GET' && path === '/v1/describe') return describe();
  if (method === 'POST' && path === '/v1/customers/search') return findCustomers(body);
  if (method === 'GET' && (m = path.match(/^\/v1\/customers\/([^/]+)$/)))
    return customers[m[1]] ? ok(customerView(customers[m[1]])) : notFound();
  if (method === 'GET' && (m = path.match(/^\/v1\/customers\/([^/]+)\/transactions$/))) {
    if (!customers[m[1]]) return notFound();
    const mine = (o) => o.customer_ref === m[1];
    return ok({ transactions: Object.values(transactions).filter(mine).slice(0, 50),
      subscriptions: Object.values(subscriptions).filter(mine).slice(0, 50) });
  }
  if (method === 'GET' && (m = path.match(/^\/v1\/transactions\/([^/]+)$/)))
    return transactions[m[1]] ? ok(transactions[m[1]]) : notFound();
  if (method === 'GET' && (m = path.match(/^\/v1\/subscriptions\/([^/]+)$/)))
    return subscriptions[m[1]] ? ok(subscriptions[m[1]]) : notFound();
  if (method === 'POST' && path === '/v1/refunds') return refund(key, body);
  if (method === 'POST' && (m = path.match(/^\/v1\/subscriptions\/([^/]+)\/cancel$/))) return cancel(key, m[1], body);
  if (method === 'GET' && (m = path.match(/^\/v1\/operations\/([^/]+)$/))) return getOperation(m[1]);
  return notFound();
}

createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const raw = Buffer.concat(chunks);
    let [status, answer] = [401, { error: { code: 'credentials_rejected', message: 'bad signature', retryable: false } }];
    if (verified(req, raw)) { // verify the raw bytes before parsing anything
      let body;
      try {
        body = raw.length ? JSON.parse(raw) : undefined;
      } catch {
        body = null;
      }
      [status, answer] = body === null ? invalid('the body is not JSON')
        : route(req.method, req.url.split('?')[0], String(req.headers['idempotency-key'] ?? ''), body);
    }
    res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify(answer));
  });
}).listen(PORT, () => console.log(`merchant-provider/v1 (REST) on http://localhost:${PORT}`));

Örnekte olmayan ama gerçek bir sunucuda bulunması gerekenler:

  • Kalıcılık (persistence). Anahtarlar ve sonuçları, sunucu yeniden başlatıldığında kaybolmamalı ve en az 30 gün saklanmalıdır (bölüm 4). Anahtarı, kaydettiği iadeyle aynı veritabanı işleminde (transaction) saklayın.
  • in_progress. İlk çağrısı hâlâ süren bir anahtarla gelen ikinci çağrı yeniden yürütülmemeli, 409 in_progress döndürmelidir. Bunun için anahtar başına bir kilit yeterlidir.
  • Bekleyen sonuçlar. İşlemcinizin daha sonra onaylayacağı bir iade pending döndürür; get_operation sonuç netleşene kadar bu durumu bildirir.
  • Tüm retler. Örnek yalnızca uyumluluk kitinin tetiklediği durumları reddeder. Gerçek bir sunucu ayrıca subscription_not_active ile (şu anda iptal edemediği bir abonelik için) ve kurallarının bir işlemi durdurduğu yerlerde kendi koruma kodlarıyla (account_restricted, merchant_hold, x_…) da reddeder.
  • Canlı ortamın gizli anahtarı. Canlı ortamdaki (production) uç noktanız srmp_ imzalama anahtarıyla doğrulama yapar ve test ortamı (sandbox) anahtarı srmps_'yi reddetmelidir. Anahtar rotasyonu sırasında hem eski hem yeni srmp_ anahtarını kabul eder (bölüm 3).
  • Temel yol. Örnek, yolları kök dizinde sunar. Bir önekin arkasında çalışıyorsanız imzayı, gönderildiği şekliyle tam yol üzerinden doğrulayın; ardından yönlendirme için öneki kaldırın.
  • Katı doğrulama. İstek gövdelerini sözleşmenin şemalarına göre doğrulayın ve okuyamadığınız bir gövdeyi 400 invalid_request ile reddedin.

Sunucuyu Test Etmek

Uyumluluk kitini (indirin) sunucuya karşı çalıştırın; localhost'ta düz http kullanmak için -allow-private bayrağını ekleyin:

sh
export PROVIDERCHECK_SANDBOX_SIGNING_SECRET=srmps_…    # the server's SIGNING_SECRET
./providercheck -sandbox -allow-private \
  -base-url http://localhost:8080 \
  -tenant 0199a3c1-0a0b-7c0d-8e0f-101112131415 \
  -customer cus_1001 -transaction ch_1001 -subscription sub_1001

İki çalıştırma arasında örnek sunucuyu yeniden başlatın: kit sub_1001'i iptal eder ve ch_1001'in bir kısmını iade eder. Ardından kiti kendi test ortamınıza (sandbox) karşı çalıştırıp raporunu yükleyin ve paneldeki canlı sözleşme kontrolünü çalıştırın (bölüm 10, Uyumluluğu kanıtlamak).