Ö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:
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-Keyyalnızca deneme çalıştırması (dry run) olmayanrefundvecancel_subscriptionisteklerinde gönderilir. Okuma istekleri,get_operationve 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ıcaX-SR-External-Ticketbaşlığı eklenir.- Gövdeler JSON biçimindedir ve yalnızca
POSTisteklerinde gönderilir. YanıtınızıContent-Type: application/jsonile 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:
{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):
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:
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=1b03ea29388ff9f1dd2bbf894f1e6e359f0fa1003ca246c678ad485249503070Okuma 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:
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=d243cf01428686ccff9f852f3f3af7af4fd7b608aa72c8d23bf9dc1ca8c2f70eDoğrulayıcınızı yayına almadan önce her iki örnekle de test edin. Aynı doğrulama, birkaç satır Node ile:
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:
{"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,5xxveya409 in_progressyanı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 birX-SR-Request-Idgönderilir.Retry-Afterdeğ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,
describeveget_operationiçin 5 sn;refundvecancel_subscriptioniçin 20 sn.
Her hata sınıfı için kopyalamaya hazır gövdeler:
{"error": {"code": "invalid_request", "message": "amount.amount_minor must be at least 1.", "retryable": false}}{"error": {"code": "not_found", "message": "No such transaction.", "retryable": false}}{"error": {"code": "idempotency_conflict", "message": "This key was used with other parameters.", "retryable": false}}{"error": {"code": "in_progress", "message": "The first request with this key is still running.", "retryable": true}}{"error": {"code": "rate_limited", "message": "Too many requests; retry after a second.", "retryable": true}}{"error": {"code": "internal_error", "message": "The billing database did not answer.", "retryable": true}}{"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:
{"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:
{"error": {"code": "merchant_hold", "message": "Finance hold on this account.", "retryable": false}}{"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.
GET /v1/describe200:
{
"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:
{
"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_attemptsoperasyonunu vepayment_attemptsyeteneğini ekler) veya"1.2"(isteğe bağlı abonelik alanlarını ekler).- Yalnızca desteklediklerinizi bildirin.
partial_refundsyoksa Chargeless her zaman yalnızca kalan tutarın tamamını ister;cancel_subscriptionyoksaget_subscriptionvecancel_subscriptionoperasyonları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.
POST /v1/customers/search{"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:
{"order_ref": "ord_5521", "card_last4": "4242"}200, en fazla 10 aday:
{"candidates": [{"customer_ref": "cus_7Hk2Lm"}]}Eşleşme yoksa da yanıt 200'dür, asla 404 değildir:
{"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.
GET /v1/customers/cus_7Hk2Lm200:
{
"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
nullolur. Chargeless kendi iadelerini bu sayede yalnızca bir kez sayar. emailsvephonesyalnı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.
GET /v1/customers/cus_7Hk2Lm/transactions200; ödemelerden ve aboneliklerden en fazla 50'şer adet, tümü istenen müşteriye ait:
{
"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:
{"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.
GET /v1/transactions/ch_1001200, gerçekleşmiş tüm iadelerle birlikte:
{
"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_atile 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):
4999geçerlidir;49.99,4999.0ve"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.
GET /v1/subscriptions/sub_88200:
{
"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:
{
"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.
POST /v1/refunds
Idempotency-Key: 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}İade tamamlandığında 200:
{
"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:
{
"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:
{"transaction_ref": "ch_1001", "amount": {"amount_minor": 3499, "currency": "USD"}, "reason": "duplicate", "dry_run": true}{"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.
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{"at_period_end": true, "reason": "forgot_to_cancel", "dry_run": false}200:
{
"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:
{"idempotency_key": null, "status": "would_succeed", "subscription_ref": "sub_88", "at_period_end": false, "effective_at": null}Zaten iptal edilmiş bir abonelik:
{"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.
GET /v1/operations/0199a3c1-0a0b-7c0d-8e0f-101112131415:ticket:0199e2c4-5b6a-7c8d-9e0f-a1b2c3d4e5f6:refund:0199e2c4-7777-7c8d-9e0f-a1b2c3d4e5f6Gerçekleşmiş bir iade için 200:
{
"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:
{
"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ı):
{
"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):
{
"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:
{"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:
POST /v1/payment-attempts/list{"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:
{
"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.
SIGNING_SECRET=srmps_… node server.mjs # serves http://localhost:8080// 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_progressdöndürmelidir. Bunun için anahtar başına bir kilit yeterlidir.- Bekleyen sonuçlar. İşlemcinizin daha sonra onaylayacağı bir iade
pendingdöndürür;get_operationsonuç 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_activeile (ş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 yenisrmp_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_requestile 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:
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).