İ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. Yani filter[created_at][gte]=… yazan istemci parantezleri kodlamadan göndermeli ve aynı dizeyi imzalamalıdır. Kütüphaneniz parantezleri %5B/%5D yaparsa 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(""), yani e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
  • 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-Timestamp1767225600
MetotGET
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-Signaturebaba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81
Kanonik dize (\n görünür yazılmıştır)
1767225600\nGET\n/v1/invoices?filter[created_at][gte]=2026-01-01T00:00:00Z&page_size=25&sort=-created_at\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

UTF-8 gövdeli yazma (POST)

X-Api-Timestamp1767225600
MetotPOST
Yol + sorgu/v1/contacts
Gövde{"title":"Örnek Müşteri A.Ş.","tax_number":"1234567890"}
hex(sha256(gövde))2d08db5717399791fe72f49d2fcdad2aaa6ed848aa000fcdc2e3152146cca3c0
X-Api-Signature4b47f1af436377c074011e1f0dca5e56300769c51fe2468f6ef37772e146562c
Kanonik dize (\n görünür yazılmıştır)
1767225600\nPOST\n/v1/contacts\n2d08db5717399791fe72f49d2fcdad2aaa6ed848aa000fcdc2e3152146cca3c0

C#

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.

C#
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
<?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: baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81

Python

Python
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: baba0dbaaaf328b3139589b11a897791b0835b5941d6cb5ab1bf2888f7f7dc81

Node.js

JavaScript
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