Ana içeriğe geç
YouSoftGeliştirici Portalı
Dokümanlar

İmzalama ve test vektörü

Bu sayfa her isteğin nasıl imzalanacağını anlatır. İmza, istek içeriğinin güvenlik anahtarıyla alınmış HMAC-SHA256 değeridir ve her istekle gönderilir. Kendi kodunuzu sayfadaki test vektörüyle doğrulayın: 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. HMAC anahtarı, güvenlik anahtarı metninin UTF-8 baytlarıdır: 43 karakterlik metni olduğu gibi kullanın, base64 olarak çözmeyin. Zaman damgası imzanın içindedir: yakalanan bir imza yeni bir damgayla tekrar oynatılamaz.

Altı kural

İmza hatalarının çoğu şu altı kuraldan biri yüzünden çıkar:

  1. Sorgu dizesi ham alınır. Sunucu sorguyu telden geldiği gibi, çözmeden imzaya koyar; imzaladığınız dize ile gönderdiğiniz dize bayt bayt aynı olmalıdır. Önerilen yol filter[created_at][gte]=… parantezlerini kodlamadan göndermek ve aynı dizeyi imzalamaktır. Kütüphaneniz imzadan sonra parantezleri %5B/%5D yaparsa sunucu başka bir metin imzalar ve her filtreli istek 401 döner; yanıt sebebi söylemez. curl kullanıyorsanız -g (--globoff) verin, yoksa curl parantezleri kendi desen sözdizimi sanar.
  2. Değerdeki + kodlanır. Sorgu çözülürken + boşluk sayılır: 2026-01-01T00:00:00+03:00 gönderilirse süzgeç 400 validation_failed alır. Saat farkını %2B03:00 olarak yazın ve imzaya da bu hâliyle koyun, ya da UTC (Z) kullanın.
  3. Sorgu yoksa soru işareti de yoktur. /v1/contacts, hiçbir zaman /v1/contacts?.
  4. Boş gövdenin özeti boş dize değildir. GET isteklerinde dördüncü alan sha256(""), yani e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
  5. Gövde UTF-8 baytlarıyla özetlenir ve gönderilen bayt dizisiyle birebir aynı olmalıdır. Türkçe karakter taşıyan bir gövdeyi imzaladıktan sonra yeniden serileştirmeyin.
  6. Saat ±5 dakika. Zaman damgası sunucu saatinden iki yönde en çok 300 saniye sapabilir. Sapan bir sunucu saati, geçerli bir imzayı da 401 yapar; sunucunuzun saatini NTP ile eşitleyin.

Yayımlanmış test vektörü

Anahtar (gizli değildir): ak_ornek_2f8c1d4b9a6e0357, güvenlik anahtarı: sk_ornek_7c2a9e1f4b8d6035a1c7e9f2b4d68a03. İkisi de yalnız bu vektör içindir, hiçbir ortamda geçerli değildir. Kendi kodunuzla aşağıdaki iki isteğin imzasını üretin; hex birebir tutmalıdır.

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

Dört dilde örnek kod

Dördü de aynı işi yapar: imzayı üretir ve ilk vektörün beklenen hex değerini yazdırır.

C#

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 giden 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ştirebilir. İmzayı isteğin
        // RequestUri değerinden değil, adrese verdiğiniz dizeden üretin:
        // aşağıdaki gibi aynı 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 aynı 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ü kullanmayın: 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 iki kez serileştirmeyin: imzaladığınız byte dizisi ile
// gövdeye yazdığınız byte dizisi aynı 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

Sık sorulanlar

Token alma ya da yenileme var mı?

Hayır. Bu API’de oturum açma ucu, erişim jetonu (token), süre dolumu ve yenileme döngüsü yoktur. Kimlik doğrulama her istekte, istekle birlikte yapılır: X-Api-Key kimliği söyler, X-Api-Signature o isteğin içeriğini (metot, yol ve sorgu, gövde, zaman damgası) güvenlik anahtarıyla imzalar. Saklanacak bir oturum olmadığı için “jetonun süresi doldu” diye bir hata da yoktur.

Yakalanan bir imza yeniden gönderilebilir mi?

Sunucu daha önce gördüğü imzaları saklamaz (tek kullanımlık değer yok). Ele geçirilen imzalı bir istek, 5 dakikalık pencere içinde aynen yeniden gönderilebilir; metodu, yolu, sorguyu, gövdeyi ya da zaman damgasını değiştirmek imzayı bozar. Yinelenen yazma isteği ikinci kez işlenir.

Çift işlemi durduran mekanizma Idempotency-Key başlığıdır ve yalnız POST /v1/invoices/{id}/send ucu onu okur (24 saat saklanır, ayrıntı: Sorgu sözleşmesi). Diğer yazma uçlarında zaman aşımından sonra yeniden göndermeden önce kaydı okuyup durumunu doğrulayın; cari kartta kendi code değerinizi göndermek ikinci denemeyi 409 ile durdurur.

Güvenlik anahtarını yenilersem imza bozulur mu?

Yenilemeden sonra eski değer 24 saat daha kabul edilir; geçiş sırasında iki değerle de imzalanan istek geçer. Aynı anda en çok iki değer geçerlidir. Ayrıntı ve sızıntıda izlenecek sıra için API anahtarı yönetimi sayfasına bakın.

OAuth neden yok?

v1’de OAuth 2.0 akışı yoktur: ne client_credentials, ne authorization_code. Yüzey sunucudan sunucuya çalışan bir entegrasyon içindir ve anahtar ile imza aynı işi bir jeton ucu, yenileme döngüsü ve süre yönetimi olmadan görür. Parola ile oturum açma akışı (password grant) da sunulmaz: kullanıcının panel parolasını üçüncü tarafa verdirir.

Bunun sonucu: son kullanıcı adına yetkilendirme (bir kullanıcının “bu uygulamaya izin ver” demesi) yoktur. Anahtarı firma yetkilisi ya da bayi açar; kapsamı üretim sırasında belirlenir.

API tarayıcıdan çağrılabilir mi?

Hayır. API CORS başlığı göndermez ve bu bilinçli bir tercihtir: güvenlik anahtarıyla imzalanan bir API’nin tarayıcıda çalışan bir istemcisi olamaz, çünkü güvenlik anahtarı tarayıcıya indiği anda gizli olmaktan çıkar. İstekleri kendi sunucunuzdan atın.