İmzalama ve test vektörü
Her istek HMAC-SHA256 ile imzalanır. Aşağıdaki vektörü kendi kodunuzla üretin: hex birebir tutmuyorsa kanonik dizeniz sapıyor demektir.
Kanonik dize
İmzalanan metin dört alandan oluşur ve alanlar tek bir LF (\n) ile ayrılır — CRLF değil:
<timestamp>\n<METOD>\n<yol+sorgu>\n<hex(sha256(gövde))>İmza X-Api-Signature başlığına küçük harf hex olarak yazılır. Zaman damgası imzanın İÇİNDEDİR: yakalanan bir imza yeni bir damgayla tekrar oynatılamaz.
Beş kural — desteğe düşen hataların tamamı buradan çıkar
- Sorgu dizesi HAM alınır. Sunucu
r.URL.RawQuery’i olduğu gibi imzaya koyar. Yanifilter[created_at][gte]=…yazan istemci parantezleri kodlamadan göndermeli ve aynı dizeyi imzalamalıdır. Kütüphaneniz parantezleri%5B/%5Dyaparsa sunucu başka bir metin imzalar ve her filtreli istek 401 döner — yanıt sebebi söylemez. - Sorgu yoksa soru işareti de yoktur.
/v1/contacts, hiçbir zaman/v1/contacts?. - Boş gövdenin özeti boş dize değildir. GET isteklerinde dördüncü alan
sha256(""), yanie3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. - Gövde UTF-8 byte’larıyla özetlenir ve gönderilen byte dizisiyle birebir aynı olmalıdır. Türkçe karakter taşıyan bir gövdeyi imzaladıktan sonra yeniden serileştirmeyin.
- Saat ±5 dakika. Sapan bir sunucu saati, geçerli bir imzayı da 401 yapar.
Yayımlanmış test vektörü
Anahtar (gizli değildir): ak_ornek_2f8c1d4b9a6e0357 — sır: sk_ornek_7c2a9e1f4b8d6035a1c7e9f2b4d68a03. İkisi de yalnız bu vektör içindir, hiçbir ortamda geçerli değildir.
Köşeli parantezli süzgeç (GET, boş gövde)
| X-Api-Timestamp | 1767225600 |
|---|---|
| Metot | GET |
| Yol + sorgu | /v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at |
| Gövde | (boş) |
| hex(sha256(gövde)) | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| X-Api-Signature | baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81 |
1767225600\nGET\n/v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855UTF-8 gövdeli yazma (POST)
| X-Api-Timestamp | 1767225600 |
|---|---|
| Metot | POST |
| Yol + sorgu | /v1/contacts |
| Gövde | {"title":"Örnek Müşteri A.Ş.","tax_number":"1234567890"} |
| hex(sha256(gövde)) | 2d08db5717399791fe72f49d2fcdad2aaa6ed848aa000fcdc2e3152146cca3c0 |
| X-Api-Signature | 4b47f1af436377c074011e1f0dca5e56300769c51fe2468f6ef37772e146562c |
1767225600\nPOST\n/v1/contacts\n2d08db5717399791fe72f49d2fcdad2aaa6ed848aa000fcdc2e3152146cca3c0C#
TR ERP dünyası .NET ağırlıklı olduğu için ilk örnek burada. Dikkat: Uri sınıfı sorguyu normalleştirebilir — imzaya verdiğiniz dizeyi adres olarak da aynen kullanın.
using System;
using System.Globalization;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
public static class YouSoftImza
{
// Kanonik dize: "<timestamp>\n<METOD>\n<yol+sorgu>\n<hex(sha256(gövde))>"
// yolSorgu, isteğin TELDEN GİDEN hâlidir: parantezler kodlanmaz.
public static string Uret(string secret, long timestamp, string metot, string yolSorgu, byte[] govde)
{
var govdeOzeti = Hex(SHA256.HashData(govde ?? Array.Empty<byte>()));
var metin = $"{timestamp}\n{metot}\n{yolSorgu}\n{govdeOzeti}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
return Hex(hmac.ComputeHash(Encoding.UTF8.GetBytes(metin)));
}
private static string Hex(byte[] b) => Convert.ToHexString(b).ToLowerInvariant();
public static HttpRequestMessage Istek(string taban, string anahtar, string secret,
HttpMethod metot, string yolSorgu, string govde)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var bayt = Encoding.UTF8.GetBytes(govde ?? "");
// ⚠ new Uri(...) sorguyu NORMALLEŞTİREBİLİR. İmzayı, gövdeye
// koyduğunuz dizeden değil, isteğin RequestUri'sinden üretmeyin:
// aşağıdaki gibi AYNI dizeyi hem imzaya hem adrese verin.
var istek = new HttpRequestMessage(metot, taban + yolSorgu);
if (bayt.Length > 0)
{
istek.Content = new ByteArrayContent(bayt);
istek.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/json") { CharSet = "utf-8" };
}
istek.Headers.Add("X-Api-Key", anahtar);
istek.Headers.Add("X-Api-Timestamp", timestamp.ToString(CultureInfo.InvariantCulture));
istek.Headers.Add("X-Api-Signature", Uret(secret, timestamp, metot.Method, yolSorgu, bayt));
return istek;
}
}
// Test vektörü — çıktı dokümandaki hex ile birebir eşleşmelidir.
class Program
{
static void Main()
{
const string secret = "sk_ornek_7c2a9e1f4b8d6035a1c7e9f2b4d68a03";
Console.WriteLine(YouSoftImza.Uret(secret, 1767225600L, "GET",
"/v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at",
Array.Empty<byte>()));
// beklenen: baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81
}
}PHP
<?php
function yousoft_imza(string $secret, int $timestamp, string $metot, string $yolSorgu, string $govde): string
{
$metin = $timestamp . "\n" . $metot . "\n" . $yolSorgu . "\n" . hash('sha256', $govde);
return hash_hmac('sha256', $metin, $secret);
}
// ⚠ http_build_query() parantezleri %5B/%5D olarak kodlar. Sorguyu ELLE
// kurun ve AYNI dizeyi hem imzaya hem cURL adresine verin.
echo yousoft_imza(
'sk_ornek_7c2a9e1f4b8d6035a1c7e9f2b4d68a03',
1767225600,
'GET',
'/v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at',
''
), PHP_EOL;
// beklenen: baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81Python
import hashlib
import hmac
import time
def yousoft_imza(secret: str, timestamp: int, metot: str, yol_sorgu: str, govde: bytes) -> str:
metin = f"{timestamp}\n{metot}\n{yol_sorgu}\n{hashlib.sha256(govde).hexdigest()}"
return hmac.new(secret.encode(), metin.encode(), hashlib.sha256).hexdigest()
# ⚠ requests'in params= sözlüğünü KULLANMAYIN: kodlaması imzaladığınız
# dizeden ayrışabilir. Sorguyu URL'in içine yazın ve aynı dizeyi imzalayın.
print(yousoft_imza(
"sk_ornek_7c2a9e1f4b8d6035a1c7e9f2b4d68a03",
1767225600,
"GET",
"/v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at",
b"",
))
# beklenen: baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81Node.js
import { createHash, createHmac } from 'node:crypto';
function yousoftImza(secret, timestamp, metot, yolSorgu, govde) {
const ozet = createHash('sha256').update(govde).digest('hex');
return createHmac('sha256', secret)
.update(`${timestamp}\n${metot}\n${yolSorgu}\n${ozet}`)
.digest('hex');
}
// ⚠ Gövdeyi İKİ KEZ serileştirmeyin: imzaladığınız byte dizisi ile
// gövdeye yazdığınız byte dizisi AYNI olmalı (JSON.stringify tek kez).
console.log(
yousoftImza(
'sk_ornek_7c2a9e1f4b8d6035a1c7e9f2b4d68a03',
1767225600,
'GET',
'/v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at',
Buffer.from('', 'utf8'),
),
);
// beklenen: baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81