مقاله آموزشی

API نرخ ارز در PHP، پایتون و Node.js؛ کد سمت سرور که با قطعی هم کار می‌کند

نویسنده: تیم نِت اَرز ۱۱ دقیقه مطالعه ۱۰ بازدید

فاکتوری که ساعت دو بامداد با کرون (cron) صادر می‌شود، ربات تلگرامی که به «دلار چند شد؟» جواب می‌دهد، یا فروشگاهی که قیمت محصول دلاری را هنگام ثبت سفارش به تومان می‌برد؛ هیچ‌کدام مرورگری ندارند که ویجت نشان بدهد. این‌جا نرخ را باید خود سرور بگیرد، نگه دارد و اگر یک روز وب‌سرویس جواب نداد، با عدد قبلی کارش را ادامه بدهد.

این مقاله دربارهٔ همین طرف ماجراست: گرفتن نرخ از API نرخ ارز نِت اَرز در PHP، پایتون و Node.js، طوری که با قطعی شبکه نخوابد و سهمیهٔ روزانه را هم هدر ندهد. هر سه نمونه خلاصه‌ای از کدهای مخزن متن‌باز ما در گیت‌هاب هستند و نسخهٔ کاملشان را همان‌جا پیدا می‌کنید. اگر هدف شما فقط نمایش یک تابلو در صفحه است، نمایش نرخ ارز زنده در سایت راه کوتاه‌تری جلوی پایتان می‌گذارد.

پیش از اولین خط کد: اپ، کلید و IP سرور

در پنل «API نرخ ارز» یک اپ بسازید و دامنهٔ خودتان را رویش ثبت کنید. کلیدی که می‌گیرید با fx-ntz-v1- شروع می‌شود و فقط همان یک بار کامل نمایش داده می‌شود؛ جایی امن نگهش دارید. مالکیت دامنه را هم یک بار با رکورد TXT یا فایل آماده تأیید کنید. قدم‌ها با تصویر در شروع سریع مستندات آمده‌اند.

قدم سومی که بیشتر برنامه‌نویس‌ها جا می‌اندازند: درخواست سرور هدر Origin ندارد، پس نِت اَرز آن را با IP فرستنده می‌سنجد. IP خروجی سرورتان را در «IPهای مجاز» همان اپ بگذارید. اگر نمی‌دانید چیست، یک بار درخواست بفرستید؛ پاسخ خطا دقیقاً همان IPی را که ما دیده‌ایم در فیلد ip برمی‌گرداند.

طرح رایگان فقط عضویت می‌خواهد. در زمان نوشتن این مقاله، نرخ همهٔ ارزها را با پانزده دقیقه تأخیر می‌دهد، با سقف ۶۰ درخواست در دقیقه و ۳۰۰۰ درخواست در روز. نرخ زنده، تاریخچه و آرشیو روزانه در طرح پرو است که برای هر اپ ماهانه ۵ دلار حساب می‌شود. عددهای روز را همیشه از صفحهٔ طرح‌ها بخوانید.

پاسخ را یک بار درست بخوانید

همهٔ نمونه‌های پایین یک پاسخ را می‌خوانند، پس اول شکلش را ببینیم. /rates?codes=USD,EUR,AED برای هر ارز یک ردیف در data برمی‌گرداند و یک بخش meta که می‌گوید نرخ مال چه لحظه‌ای است:

فیلدمعنینکته
buy / sellنرخ خرید و فروش به توماناز دید صرافی: sell همان عددی است که شما برای خرید ارز می‌پردازید
midمیانگین خرید و فروشبرای نمایش و مقایسه، نه برای فاکتور
unitتعداد واحد ارز که قیمت برایش آمدهبیشتر ارزها ۱؛ دینار عراق ۱۰۰. قیمت یک واحد = عدد ÷ unit
change_24h_percentدرصد تغییر بیست‌وچهار ساعتهمی‌تواند منفی باشد
meta.as_ofزمان واقعی نرخهمین را کنار عدد نشان بدهید
meta.is_delayed / delayed_minutesتأخیر طرح شمادر طرح رایگان روشن است

اشتباه رایج این است که unit را نادیده بگیرند. کدی که امروز فقط دلار و یورو را می‌خواند، روزی که کسی دینار عراق را اضافه کند، عددی صد برابر نشان می‌دهد.

PHP خالص، مناسب هاست اشتراکی

این تابع فقط افزونهٔ cURL و یک پوشهٔ قابل نوشتن لازم دارد. دو کار مهم می‌کند: جواب سالم را روی دیسک کش می‌کند تا هر بازدید یک درخواست نفرستد، و اگر وب‌سرویس یا شبکه جواب نداد، آخرین نسخهٔ سالم را برمی‌گرداند؛ هرچقدر هم قدیمی باشد، از یک صفحهٔ خالی بهتر است.

<?php
function netarz_fx(string $path, array $query = [], int $ttl = 180): array
{
    $key = getenv('NETARZ_FX_KEY') ?: throw new RuntimeException('Set NETARZ_FX_KEY first.');
    $url = 'https://netarz.ir/api/fx/v1'.$path.($query ? '?'.http_build_query($query) : '');
    $cacheFile = sys_get_temp_dir().'/netarz-fx-'.md5($url).'.json';

    if (is_file($cacheFile) && time() - filemtime($cacheFile) < $ttl) {
        return json_decode((string) file_get_contents($cacheFile), true);
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, // the IP we see must match your allow-list
        CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$key, 'Accept: application/json'],
    ]);
    $raw = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);

    $body = is_string($raw) ? json_decode($raw, true) : null;
    if ($status === 200 && is_array($body)) {
        file_put_contents($cacheFile, $raw, LOCK_EX);
        return $body;
    }
    if (is_file($cacheFile)) { // stale beats nothing
        return json_decode((string) file_get_contents($cacheFile), true);
    }
    $error = $body['error'] ?? ['code' => 'network_error', 'message' => 'no response'];
    throw new RuntimeException($error['code'].': '.$error['message']);
}

$board = netarz_fx('/rates', ['codes' => 'USD,EUR,AED']);
foreach ($board['data'] as $row) {
    printf("%s buy %s sell %s (per %d)\n", $row['code'], number_format($row['buy']), number_format($row['sell']), $row['unit']);
}

خط CURLOPT_IPRESOLVE را برندارید. netarz.ir آدرس IPv6 هم دارد و سروری که هر دو نسخه را دارد، معمولاً از IPv6 وصل می‌شود؛ یعنی IPv4ای که در پنل ثبت کرده‌اید دیده نمی‌شود و خطای ip_not_allowed می‌گیرید. اگر لاراول دارید، سرویس آمادهٔ NetArzFx در همان مخزن همین منطق را با Cache::remember و force_ip_resolve پیاده کرده است.

پایتون: یک کلاس کوچک با پیام خطای خوانا

در پایتون همه‌چیز را در یک کلاس جمع کنید تا کش و سربرگ کلید یک جا بمانند. دو خط اول، اتصال را روی IPv4 نگه می‌دارند؛ اگر IPv6 سرور را در پنل ثبت کرده‌اید، حذفشان کنید.

import os, socket, time
import requests
import urllib3.util.connection as urllib3_connection

urllib3_connection.allowed_gai_family = lambda: socket.AF_INET  # IPv4 only


class NetArzFx:
    BASE = "https://netarz.ir/api/fx/v1"

    def __init__(self, key=None, ttl=120):
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {key or os.environ['NETARZ_FX_KEY']}"
        self.ttl, self._cache = ttl, {}

    def _get(self, path, **params):
        cache_key = path + repr(sorted(params.items()))
        hit = self._cache.get(cache_key)
        if hit and time.time() - hit[0] < self.ttl:
            return hit[1]
        r = self.session.get(self.BASE + path, params=params, timeout=10)
        body = r.json()
        if not r.ok:
            err = body.get("error", {})
            raise RuntimeError(f"{err.get('code')}: {err.get('message')} (seen IP: {err.get('ip')})")
        self._cache[cache_key] = (time.time(), body)
        return body

    def rates(self, codes):
        return self._get("/rates", codes=",".join(codes))

    def convert(self, source, target, amount=1, side="mid"):
        return self._get("/convert", **{"from": source, "to": target, "amount": amount, "side": side})["data"]


fx = NetArzFx()
for row in fx.rates(["USD", "EUR", "AED"])["data"]:
    print(row["code"], row["buy"], row["sell"], "per", row["unit"])

کش داخل حافظه برای اسکریپتی که مدام در حال اجراست (مثل ربات) کافی است. اگر اسکریپت با کرون هر بار از نو اجرا می‌شود، کش حافظه هر بار خالی است؛ آن‌جا جواب را در یک فایل یا Redis نگه دارید.

Node.js بدون هیچ وابستگی

از نسخهٔ ۱۸ به بعد، fetch داخل خود Node.js است و به هیچ کتابخانه‌ای نیاز ندارید. معادل IPv4 در این‌جا یک خط است:

import dns from "node:dns";
dns.setDefaultResultOrder("ipv4first");

const BASE = "https://netarz.ir/api/fx/v1";
const headers = { Authorization: `Bearer ${process.env.NETARZ_FX_KEY}` };
let cache = { at: 0, body: null };

export async function board(codes = ["USD", "EUR", "AED"]) {
  if (cache.body && Date.now() - cache.at < 2 * 60 * 1000) return cache.body;
  const res = await fetch(`${BASE}/rates?codes=${codes.join(",")}`, { headers });
  const body = await res.json();
  if (!res.ok) {
    const { code, message, ip } = body.error ?? {};
    throw new Error(`${code}: ${message}${ip ? ` (seen IP: ${ip})` : ""}`);
  }
  cache = { at: Date.now(), body };
  return body;
}

تبدیل مبلغ: از دلار به تومان و برعکس

برای تبدیل، لازم نیست خودتان ضرب و تقسیم کنید و نگران unit باشید. مسیر /convert مبلغ را می‌گیرد و نتیجه را برمی‌گرداند. تومان با کد IRT و ریال با IRR مشخص می‌شود، و با side می‌گویید کدام نرخ ملاک باشد:

# 250 USD to Toman at the sell rate (what a buyer pays for dollars)
curl -sS -4 "https://netarz.ir/api/fx/v1/convert?from=USD&to=IRT&amount=250&side=sell" \
  -H "Authorization: Bearer $NETARZ_FX_KEY"

# and back: how many dollars is a Toman amount
curl -sS -4 "https://netarz.ir/api/fx/v1/convert?from=IRT&to=USD&amount=50000000" \
  -H "Authorization: Bearer $NETARZ_FX_KEY"

برای قیمت فروش یک کالای دلاری، sell منطقی‌تر است، چون همان نرخی است که خودتان برای تهیهٔ دلار می‌پردازید. برای نمایش خبری، mid کافی است. هر نرخی که انتخاب کردید، در متن فاکتور یا صفحه بنویسید کدام است؛ فرق خرید و فروش گاهی چند درصد است و خریدار حق دارد بداند. اگر هنوز روشن نیست این اعداد از کدام بازار می‌آیند، چهار نرخ ارز در ایران را بخوانید.

چهار پاسخ خطا که اول کار می‌بینید

  • 401 missing_api_key: کلید در سربرگ Authorization نرسیده. کلید را در نشانی (?key=) نفرستید؛ وب‌سرویس فقط سربرگ را می‌پذیرد.
  • 403 origin_required یا ip_not_allowed: درخواست از سرور آمده و IP آن در فهرست مجاز نیست. همان IPی را که در فیلد ip پاسخ آمده ثبت کنید.
  • 402 pro_required: مسیری را صدا زده‌اید که فقط در طرح پرو است، مثل تاریخچه.
  • 429 rate_limit_exceeded: سقف دقیقه‌ای پر شده. به اندازهٔ سربرگ Retry-After صبر کنید و کش را بلندتر کنید.

فهرست کامل کدها در صفحهٔ خطاهای مستندات است. یک چیز دیگر هم به کارتان می‌آید: سربرگ‌های X-Quota-Remaining و X-Quota-Reset در هر پاسخ می‌گویند از سهمیهٔ امروز چقدر مانده، پس لازم نیست برای فهمیدنش درخواست جدا بفرستید.

کرون، ربات، سرور: کجا اجرا کنید

کلید این وب‌سرویس به IP قفل است، پس جایی اجرایش کنید که IP خروجی ثابت دارد. یک سرور مجازی کوچک ساده‌ترین جواب است. روی هاست اشتراکی هم معمولاً کار می‌کند، به شرط این‌که هاست IP خروجی‌اش را عوض نکند. سرویس‌های بدون سرور (Vercel، Cloudflare Workers) و Google Apps Script IP ثابت ندارند و برای آن‌ها یک رلهٔ کوچک روی سرور ثابت لازم است که فایلش در همان مخزن هست.

اگر نرخ را هر پنج دقیقه با کرون می‌گیرید و در فایل یا پایگاه داده می‌گذارید، بقیهٔ برنامه فقط از همان‌جا بخواند. با این الگو، مصرف روزانهٔ شما حدود ۲۹۰ درخواست می‌شود، هرچقدر هم بازدید داشته باشید. برای وردپرس همین الگو به‌صورت آماده در نرخ ارز در وردپرس و ووکامرس آمده و برای صفحه‌گسترده در نرخ دلار در گوگل شیت و اکسل.

کد کامل هر سه زبان در گیت‌هاب

نمونه‌های بالا کوتاه شده‌اند تا در صفحه جا بشوند. نسخهٔ کامل، با توضیح در بالای هر فایل، در مخزن netarz/fx-api-examples است: php/، python/، javascript/node/ و سرویس لاراول در laravel/. همان‌جا curl/ همهٔ مسیرها را با cURL نشان می‌دهد، از جمله تاریخچه و استریم طرح پرو. مجوز مخزن MIT است؛ کد را در پروژهٔ تجاری خودتان هم به کار ببرید.

فهرست همهٔ مخزن‌ها، از نمونه‌کد وب‌سرویس هوش مصنوعی تا افزونهٔ وردپرس، در صفحهٔ کدهای متن‌باز نِت اَرز آمده است. اگر در کد ایرادی دیدید، در بخش Issues همان مخزن بنویسید. برای اپ، کلید و تأیید دامنه هم از پنل تیکت بزنید و شناسهٔ اپ و متن کامل پاسخ خطا را بفرستید؛ با این دو، علت را زودتر پیدا می‌کنیم.

آموزش‌های کاربردی در گیسو

گیسو برند هوش مصنوعی نِت اَرز است، با همین حساب و همین اعتبار. این راهنماها قدم‌به‌قدم نشان می‌دهند همین کار را چطور با هوش مصنوعی انجام دهید.

پرسش‌های پرتکرار

API نرخ ارز نِت اَرز رایگان است؟

طرح رایگان فقط عضویت می‌خواهد و نرخ همهٔ ارزها را با پانزده دقیقه تأخیر می‌دهد، با سقف روزانه. نرخ زنده، تاریخچه و آرشیو روزانه در طرح پرو است که برای هر اپ ماهانه حساب می‌شود. اعداد روز در صفحهٔ طرح‌های مستندات آمده است.

چرا با وجود ثبت IP سرور، خطای ip_not_allowed می‌گیرم؟

در بیشتر موارد سرور شما با IPv6 وصل شده و شما IPv4 را ثبت کرده‌اید. اتصال را روی IPv4 نگه دارید، مثل نمونه‌های این مقاله، یا همان IPی را که در فیلد ip پاسخ خطا آمده به فهرست مجاز اضافه کنید.

برای تبدیل قیمت دلاری به تومان از buy استفاده کنم یا sell؟

برای قیمت فروش کالا، sell منطقی‌تر است، چون همان نرخی است که برای تهیهٔ دلار می‌پردازید. mid برای نمایش خبری کافی است. هر کدام را انتخاب کردید، در صفحه یا فاکتور بنویسید.

نمونه‌کدها را در پروژهٔ تجاری هم می‌شود استفاده کرد؟

بله. مخزن fx-api-examples در گیت‌هاب با مجوز MIT منتشر شده و کپی، تغییر و استفادهٔ تجاری از آن آزاد است. فقط کلید خودتان را در مخزن عمومی کامیت نکنید.

محصولات مرتبط با این مقاله

مطالب مشابه

نظر خوانندگان

هنوز نظری ثبت نشده. اگر این مقاله پرسشتان را جواب داد یا جای چیزی در آن خالی ماند، همین‌جا بنویسید.

نظرتان را بنویسید

این مقاله چقدر به کارتان آمد؟ (اختیاری)

نظرها را پیش از انتشار بررسی می‌کنیم. نقد صریح مشکلی ندارد؛ تبلیغ و توهین منتشر نمی‌شود.