2026'da bordro yazılımı geliştiriyorsanız artık tek ülkelik bir araç inşa etmiyorsunuz. Deel, Remote, Rippling ve dikey HRIS platformlarının uzun kuyruğu her ay 90'dan fazla ülkedeki kontratçılara ve çalışanlara maaş gönderiyor ve bu ödemelerin her birinde aynı sıkıcı ama pahalı soru saklı: hangi döviz kurunu kullanıyoruz, ne zaman kilitliyoruz ve çalışana (ve denetçiye) doğru yaptığımızı nasıl ispat ediyoruz? Sınır ötesi bordro para birimi dönüşümü arka ofis detayı gibi gelir — ta ki 4.000 $'lık aylık maaşta 50 baz puanlık sapmanın o çalışana ayda 20 $'a — her ay — mal olduğunu ve destek kutunuzun cuma günü dolduğunu fark edene kadar.
Bu rehber, birden fazla para biriminde uzlaştırma yapan bordro, kontratçı ödemesi, EOR veya HRIS sistemleri inşa eden mühendisler için. Önemli mimari kararları — kur kaynağı, kilit tarihleri, mid-market vs spread, idempotency, denetim izi, yuvarlama — adım adım geçeceğiz ve Finexly API kullanarak her biri için Node.js, Python ve PHP'de üretim kodu yazacağız. Sonunda elinizde bir SOC 2 incelemesine ve USD/JPY gecelik %1,5 hareket ettiğinde sabaha karşı 2'de gelen Slack pingine dayanan bir bordro FX katmanı olacak.
Bordro FX'in Göründüğünden Daha Zor Olmasının Sebebi
Saf yaklaşım tek satırlık koddur: amount_local = amount_usd * rate. Pazarlama sayfasındaki bir para çevirici için yeterli. Bordro için yeterli değil — hepsi aynı anda ısıran altı nedenle:
- Kur tekrar üretilebilir olmalı. Bir çalışan ya da denetçi Mart maaşının neden ¥609.118 değil de ¥608.243 olduğunu sorduğunda, belirli bir kuru, zaman damgasını ve kaynağı işaret edebilmeniz gerekir. "Stripe'ın payout sırasında verdiği fiyat" denetimden geçmez.
- Kilit tarihi bir politika kararıdır, bug değil. Çalıştırma günü mü? Dönem sonu mu? Aylık bordro için ayın 25'i mi? Her seçeneğin FX riski, çalışanın öngörülebilirliği ve vergi raporlaması üzerinde farklı etkileri var. Kod CFO'nun seçtiği politikayı kodlamalı — ve deploy gerektirmeden değiştirilebilir olmalı.
- Mid-market ve "ödeme kuru" farklı şeyler. Mid-market kuru bid ve ask arasındaki orta noktadır — Google ya da Bloomberg'in gösterdiği. SWIFT, yerel ray veya stablecoin köprüsü üzerinden parayı fiilen hareket ettiren kur her zaman bir spread taşır. Mid-market referansını net göster, ödeme sağlayıcısının gerçekten kullandığı kuru ayrıca takip et; mutabakat ancak böyle yürür.
- Idempotency önemli. Bordro koşumları yeniden denenir — job timeout, kuyruk redelivery, operatörün iki kez tıklaması. (Çalışan, dönem) için FX sorgusu idempotent değilse retry'lar farklı kurlar verir ve farklı gross-to-net üretir.
- Hafta sonları, tatiller ve yargı kuralları. FX piyasaları cuma akşam New York'tan pazar akşamı Sydney'e kadar kapalıdır; bordro koşumları durmaz. Saf kod sessizce eski cache kullanır. Bazı yargılar (Brezilya PTAX, Arjantin BCRA, hindistan'da yerleşik olmayanlar için RBI) mid-market'i ezen merkez bankası referans kurları ister — kur katmanı yargı bazlı override desteklemeli.
Bu altısını doğru kurarsanız bir bordro FX katmanınız olur. Birini kaçırırsanız patlamaya hazır bir olay vakası elde edersiniz.
Referans Mimari
Aşağıdaki örnekler sıkı sınırları olan üç katmanı varsayar:
Katman 1 — Kur Katmanı. Gerçek zamanlı sağlayıcıdan (Finexly) mid-market kurları çeker, cache'ler, denetim izi için günde bir snapshot alır. Platformda başka hiçbir şey FX sağlayıcısıyla doğrudan konuşmaz.
Katman 2 — FX Politikası. (Çalışan, dönem, kaynak tutar, kaynak para birimi, hedef para birimi, politika) alıp (dönüştürülmüş tutar, kur, zaman damgası, kaynak) döndüren saf fonksiyonlar. "25'inde kilitle" veya "BRL için merkez bankası referansını kullan" gibi kararları kodlar. Katman 1'i çağırır; sağlayıcıyı asla.
Katman 3 — Ödeme Yürütme. Parayı hareket ettiren her şey (Stripe Connect, Wise Platform, banka rayı, stablecoin köprüsü). Sağlayıcının gerçekten uzlaştırdığı kuru raporlar — Katman 2'nin referans kurunun yanına denetim tablosuna yazılır.
Bu üç katmanlı temiz ayrım, ülke ekledikçe kod tabanını bakımı yapılabilir tutan en büyük karardır — ve Katman 1 sabit kurlarla stub'lanabildiği için test edilebilirliği uygulanabilir hâle getirir.
Kur Kaynağı Seçimi: Denetim İzli Mid-Market
Katman 1 için istediğiniz kaynak mid-market — bid ile ask arasındaki orta nokta, en az dakikada bir tazelenir, geçmişe tarih bazında sorgulanabilir. Diğer her şey için temiz bir referans verir.
Finexly büyük likidite sağlayıcılarından toplanmış mid-market kurları canlı ve geçmiş endpoint'lerle döner. Bağlantıyı doğrulamak için ilk çağrı:
curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY,INR,BRL,PHP,MXN" \
-H "Authorization: Bearer YOUR_API_KEY"rates, base ve timestamp içeren JSON döner. Bordro için önemli iki alan: timestamp (snapshot'ın UTC anı) ve her bir para biriminin değeri. Her zaman ikisini de logla — sadece kuru değil.
Sağlayıcı seçimine dair daha fazla bağlam için ücretsiz vs ücretli currency API karşılaştırması ve Finexly vs Open Exchange Rates vs Fixer karşılaştırması ödünleri detaylı işliyor.
Kuru Kilitlemek: Uygulamaya Değer Üç Politika
"Ne zaman kilitleriz" kararı bordro FX'inin kalbidir. Üç politika neredeyse her gerçek müşteriyi kapsar:
Politika A — Çalıştırma gününde kilitleme. Basit, savunulabilir, bordroda açıklaması kolay. Aynı gün kuru; Run'a basıldığında piyasa ne yapıyorsa çalışan onu görür. Kontratçı tipi ödemeler için en iyi varsayılan.
Politika B — Ayın sabit bir gününde kilitleme. Aylık bordrolu müşteri 25'inde kilitlemek isteyebilir — bordrolar 27'ye kadar üretilir, ödeme ayın 1'inde çıkar. Çalıştırma günü dalgalanmasını çalışan deneyiminden çıkarır.
Politika C — Dönem ortalaması. Uzun dönemler (yarım aylık, aylık) için bazı müşteriler dönem boyunca mid-market kurlarının ortalamasını ister. Dalgalanmayı yumuşatır, penceredeki her iş günü için geçmiş kur sorgusu gerektirir.
Üçünün TypeScript implementasyonu. Politika mantığı net görünsün diye Katman 1 çağrıları rateService.getRate(...) ile stub'lanmıştır:
// Layer 2: payroll FX policy
type Policy = "run_date" | "fixed_day" | "period_avg";
interface LockedRate {
rate: number;
source: "finexly_mid";
policy: Policy;
policyInputs: Record<string, string | number>;
lockedAt: string; // ISO8601 UTC
rateTimestamp: string; // ISO8601 UTC, from provider
}
async function lockPayrollRate(
base: string,
quote: string,
payPeriodStart: Date,
payPeriodEnd: Date,
payrollRunAt: Date,
policy: Policy,
fixedDay: number = 25
): Promise<LockedRate> {
switch (policy) {
case "run_date": {
const r = await rateService.getRate(base, quote, payrollRunAt);
return {
rate: r.rate,
source: "finexly_mid",
policy,
policyInputs: { runAt: payrollRunAt.toISOString() },
lockedAt: new Date().toISOString(),
rateTimestamp: r.timestamp,
};
}
case "fixed_day": {
const lockDate = new Date(payPeriodEnd);
lockDate.setUTCDate(fixedDay);
// If the fixed day is a weekend, snap back to Friday
const snapped = snapToBusinessDay(lockDate);
const r = await rateService.getRate(base, quote, snapped);
return {
rate: r.rate,
source: "finexly_mid",
policy,
policyInputs: { fixedDay, snappedTo: snapped.toISOString() },
lockedAt: new Date().toISOString(),
rateTimestamp: r.timestamp,
};
}
case "period_avg": {
const days = businessDaysBetween(payPeriodStart, payPeriodEnd);
const rates = await Promise.all(
days.map(d => rateService.getRate(base, quote, d))
);
const avg = rates.reduce((s, r) => s + r.rate, 0) / rates.length;
return {
rate: avg,
source: "finexly_mid",
policy,
policyInputs: {
start: payPeriodStart.toISOString(),
end: payPeriodEnd.toISOString(),
dayCount: rates.length,
},
lockedAt: new Date().toISOString(),
rateTimestamp: rates[rates.length - 1].timestamp,
};
}
}
}Dönen LockedRate nesnesinin biçimi bordro motorunun geri kalanıyla yapılan sözleşmedir. Aşağı yöndeki her hesaplama — gross-to-net, vergi stopajı, bordroda gösterilen tutar, dışa aktarılan ödeme dosyası — o tek kilitli kuru referans alır. Asla yeniden fiyatlama yapma.
Finexly'den Geçmiş Kurları Sorgulamak (Python)
Politika B ve C geçmiş kurlara ihtiyaç duyar — Finexly'nin /historical endpoint'i ISO tarih alır. Politika C için retry, backoff ve idempotent cache'li Python implementasyonu:
import os
import time
import json
import hashlib
import requests
from datetime import date, timedelta
from typing import List
import redis
API_KEY = os.environ["FINEXLY_API_KEY"]
BASE_URL = "https://api.finexly.com/v1"
r = redis.from_url(os.environ["REDIS_URL"])
def _cache_key(base: str, quote: str, on: date) -> str:
return f"fx:{base}:{quote}:{on.isoformat()}"
def get_historical_rate(base: str, quote: str, on: date) -> dict:
"""Return mid-market rate for a base/quote pair on a given UTC date."""
key = _cache_key(base, quote, on)
cached = r.get(key)
if cached:
return json.loads(cached)
url = f"{BASE_URL}/historical"
params = {"base": base, "symbols": quote, "date": on.isoformat()}
headers = {"Authorization": f"Bearer {API_KEY}"}
for attempt in range(4):
try:
resp = requests.get(url, params=params, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
payload = {
"base": data["base"],
"quote": quote,
"rate": data["rates"][quote],
"timestamp": data["timestamp"],
"date": on.isoformat(),
}
# Historical rates are immutable — cache them for 30 days
r.setex(key, 30 * 24 * 3600, json.dumps(payload))
return payload
except (requests.HTTPError, requests.ConnectionError, requests.Timeout):
if attempt == 3:
raise
time.sleep(2 ** attempt)
def business_days(start: date, end: date) -> List[date]:
days, cur = [], start
while cur <= end:
if cur.weekday() < 5: # Mon-Fri
days.append(cur)
cur += timedelta(days=1)
return days
def period_average_rate(base: str, quote: str, start: date, end: date) -> dict:
days = business_days(start, end)
if not days:
raise ValueError("No business days in period")
rates = [get_historical_rate(base, quote, d)["rate"] for d in days]
avg = sum(rates) / len(rates)
return {
"base": base,
"quote": quote,
"rate": avg,
"policy": "period_avg",
"day_count": len(days),
"first_day": days[0].isoformat(),
"last_day": days[-1].isoformat(),
}Kodda bariz olmayan ama önemli iki şey. Birincisi, geçmiş kurlar değişmezdir — Finexly'de 2026-04-03'ün USD/EUR kuru sonsuza dek aynıdır — bu yüzden 30 günlük cache güvenlidir ve tekrarlayan kontratçıları olan herhangi bir bordro sistemi için çağrı hacmini %95+ keser. İkincisi, retry döngüsü üstel backoff kullanır çünkü bordro batch'leri tipik olarak binlerce müşteride aynı pazar gecesi penceresinde çalışır ve FX sağlayıcısı paylaşılan bir kaynaktır.
Cache ve hata yönetimi kalıpları için daha derin bir okuma: currency API cache ve hata yönetimi rehberi.
Idempotency ve Denetim İzi (PHP)
Bir bordro FX katmanının en az takdir edilen yaptığı şey kilitlenen kuru bir idempotency anahtarına karşı saklamaktır — böylece yeniden çalıştırılan bordro koşumları aynı kuru yeniden kullanır. Finexly'i Postgres destekli idempotent bir servisin arkasına saran PHP implementasyonu:
<?php
declare(strict_types=1);
class PayrollFx {
public function __construct(
private \PDO $db,
private string $apiKey,
private string $baseUrl = "https://api.finexly.com/v1"
) {}
public function lockOnRunDate(
string $idempotencyKey,
string $base,
string $quote,
\DateTimeImmutable $runAt
): array {
// 1. Have we already locked this key?
$stmt = $this->db->prepare(
"SELECT rate, rate_timestamp, source FROM payroll_fx_locks
WHERE idempotency_key = :k"
);
$stmt->execute([":k" => $idempotencyKey]);
$existing = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($existing) {
return $existing + ["replay" => true];
}
// 2. Fetch fresh mid-market from Finexly
$url = sprintf(
"%s/latest?base=%s&symbols=%s",
$this->baseUrl, urlencode($base), urlencode($quote)
);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $this->apiKey],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
throw new \RuntimeException("Finexly returned $code");
}
$data = json_decode($body, true);
$rate = $data["rates"][$quote] ?? null;
if ($rate === null) {
throw new \RuntimeException("Missing $quote in response");
}
// 3. Persist atomically (UNIQUE on idempotency_key)
$ins = $this->db->prepare(
"INSERT INTO payroll_fx_locks
(idempotency_key, base_currency, quote_currency, rate,
rate_timestamp, source, locked_at, run_at)
VALUES (:k, :b, :q, :r, :rt, 'finexly_mid', NOW(), :ra)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING rate, rate_timestamp, source"
);
$ins->execute([
":k" => $idempotencyKey,
":b" => $base,
":q" => $quote,
":r" => $rate,
":rt" => $data["timestamp"],
":ra" => $runAt->format("c"),
]);
$row = $ins->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
// Race: another worker won. Re-read.
$stmt->execute([":k" => $idempotencyKey]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
}
return $row + ["replay" => false];
}
}Doğal bir idempotency anahtarı payroll_run_id:employee_id:base:quote — tek koşum içinde çakışmalar imkânsız, retry'lar (kuyruk redelivery veya operatörün çift tıklaması) ikinci bir API çağrısı olmadan tam olarak aynı kuru alır.
Daha fazla PHP entegrasyon kalıbı için currency API PHP entegrasyon rehberine bakın.
Hafta Sonları, Tatiller ve Kapalı Piyasalar
FX piyasaları cuma 17:00 New York'tan pazar 17:00 Sydney'e kadar kapalıdır. Bu pencerede üç akıllı politika: son kapanışa rollback (bordro için doğru varsayılan — istikrarlı ve açıklanabilir), bir sonraki açılışa roll forward (önizleme), veya fiyat vermeyi reddet (yüksek tutarlı tek seferlik transferler).
Finexly geçmiş endpoint'i otomatik olarak en yakın iş gününe snap'ler — cumartesi tarihi sorgularsanız cuma kapanışını alırsınız, timestamp cumayı gösterir. Her zaman dönen timestamp'e güvenin, istediğiniz tarihe değil. Yerel banka tatilleri (Brezilya Karnavalı, Hindistan Diwali, Çin Yeni Yılı) kendi tablosunu gerektirir — FX piyasası açık ama hedef ray kapalı olabilir, fon varış tarihini ayrı flag'leyin.
Yuvarlama, Görüntüleme ve Bordro Aritmetiği
Kur elinizde olduktan sonra aritmetiğin de doğru olması gerekir. Destek talebini önleyen üç kural:
- Tam hassasiyetle çarpın, sonunda bir kez yuvarlayın. En az 10 anlamlı haneli bir decimal tipiyle
amount_local = amount_usd * ratehesaplayın, sonra hedef para biriminin ISO 4217 alt birimlerine yuvarlayın. JPY 0 hane; USD/EUR 2; KWD/BHD 3; CLF 4. - Half-to-even (bankacı yuvarlaması) kullanın. Binlerce bordroyu işlerken birikmiş yanlılığı azaltır. JavaScript'in varsayılan
Math.round'u half-away-from-zero — decimal kütüphanesi (decimal.js,bignumber.js) kullanın. - Kuru yeterli hassasiyetle gösterin. Bordroda en az 6 anlamlı hane (
0,911234,0,91değil). Kuru hesap makinesine kopyalayan çalışan yerel tutarı kuruşuna kadar üretebilmeli.
Baştan Sona Bir Örnek
ABD'li bir fintech aylık kontratçı ödemesi yapıyor. Maria, Mexico City'de, ayda 4.800 USD üzerinden sözleşmeli. Müşteri politikası: "25'inde kilit, dönem sonuna hizalı, mid-market referans, ayın 1'inde Stripe Connect ile uzlaştırma."
2026-04-25'te bordro FX katmanı idempotency_key="run_2026_04:contractor_847:USD:MXN" ile çağrılır. O tarih için Finexly geçmiş USD→MXN sorgulanır, 17,8642 alınır, kilitlenir. Bordroda "USD 4.800,00 → MXN 85.748,16 17,8642 USD/MXN'den (mid-market, Finexly, 2026-04-25 21:00 UTC)" görünür.
2026-05-01'de Stripe Connect uzlaştırır. Stripe'ın payout API'si kendi exchange_rate'ini döner — diyelim 17,8201 (mid-market eksi 25 bps spread). İki kur da denetim tablosuna düşer. 1099 dışa aktarımı kilitli kuru kullanır; GL mutabakatı uzlaştırma kurunu kullanır; fark FX maliyet hesabına kaydedilir. İşte iyinin görüntüsü budur.
Kaçınılması Gereken Yaygın Hatalar
Bordro sistem kod incelemelerinde tekrar tekrar gördüğümüz kalıplar:
- Retry'de yeniden fiyatlama. Retry'de farklı kur, koşumun içeriği değişmediği halde farklı gross-to-net anlamına gelir. Her zaman idempotency anahtarıyla cache'leyin.
- Kur zaman damgası olarak
Date.now(). Bu sizin saatiniz, sağlayıcınınki değil. Sağlayıcınıntimestamp'ini logla. - Eski cache'e sessiz fallback. Kesinti sırasında cache'e düşüyorsanız bordroda etiketleyin — eski veriyi canlı gibi sunmayın.
- Para için kayan nokta.
4800 * 17.8642her makinede aynı değildir. Parayla ilgili her şey için decimal kütüphanesi. - Koşum başına tek global kur. Farklı çalışanlar farklı politikalar gerektirebilir (Brezilyalı kontratçı PTAX'ta, Hintli RBI referansında). Çalışan bazında çözün.
Sıkça Sorulan Sorular
Sınır ötesi bordro için en iyi döviz kuru hangisi? Mid-market — bid ve ask arasındaki orta nokta — standart referanstır. Çalışan Google'da ya da Bloomberg'de doğrulayabilir, çoğu sözleşmenin "adil piyasa kuru" derken kastettiği de odur. Asıl ödeme rayı mid-market'a spread ekler; ikisini de logla, bordroda mid-market'i göster.
Kuru çalıştırma tarihinde mi yoksa ödeme tarihinde mi kilitleyeyim? Müşterinin politikası hangisiyse — ikisi de savunulabilir. Çalıştırmada kilitleme ödeme öncesi çalışana sabit önizleme verir; ödemede kilitleme alttaki rayın davranışıyla uyuşur. Asıl olan politikayı koda açıkça yazmak, job'un API'yi rastgele hangi tarihte çağırdığına gizli olarak bırakmak değil.
Hafta sonu veya tatil koşumlarını nasıl ele alırım?
Son kapanışı kullan. Finexly geçmiş endpoint'i en yakın iş gününe otomatik snap'ler; timestamp'e güven. Hedef para biriminin banka tatilleri (alıcı bankanın kapalı olduğu) için bordroda fon varış tarihini flag'le ama FX kurunu normal kullan.
Bazı ülkeler için merkez bankası referans kuru kullanmam gerekiyor mu? Bazıları için evet — Brezilya (PTAX), Hindistan'da yerleşik olmayanlar için raporlama (RBI referans), Arjantin (BCRA) yerel vergi beyanlarının gerektirdiği yayınlanmış referans kurlara sahip. Kur katmanı yargı bazlı override almalı ve geri kalanlar için mid-market'a düşmeli.
Bordroda kurun ne kadar hassas olması gerek?
Görüntülemede en az 6 anlamlı hane — 17,8642, 17,86 değil. Hesaplarken 10+ haneli decimal kullan, yalnızca sonunda yuvarla. Çalışanlar kuru gerçekten hesap makinesine giriyor.
Üretim bordrosunda ücretsiz currency API kullanabilir miyim? Ücretsiz katmanlar çok düşük hacimlerde çalışır ama çoğu (yalnızca günlük kurlar, geçmiş yok, ayda 1.000 istek) ilk uluslararası işe alımda kırılan limitlere sahip. Seçenekleri ücretsiz vs ücretli currency API rehberinde karşılaştır.
Kapanış
Sınır ötesi bordro para birimi dönüşümü bir çarpma gibi görünür ama altı iç içe karara dönüşür: kaynak politikası, kilit tarihi, idempotency, yuvarlama, hafta sonu yönetimi ve denetim izi. Üç katmanlı mimariyi — Kur Katmanı, FX Politikası, Ödeme Yürütme — doğru kurun, her karar kimsenin dokunmak istemediği bir stored procedure yerine küçük ve test edilebilir bir fonksiyona dönüşür.
Gerçek zamanlı mid-market kurları bordro motorunuza bağlamaya hazır mısınız? Ücretsiz Finexly API anahtarınızı alın — kredi kartı gerekmez. Ayda 1.000 istek yukarıdaki her kod örneğini test etmeye yeter, ücretli planlar onboard edeceğiniz her kontratçıya ölçeklenir. Finexly API belgelerine göz atın ya da Finexly'i diğer sağlayıcılarla karşılaştırın.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →