Назад к блогу

Как создать мультивалютное выставление счетов с помощью API обменных курсов (руководство 2026)

V
Vlado Grigirov
July 30, 2026
Multi-Currency Currency API Exchange Rates Invoicing Developer Guide Accounting

Мультивалютное выставление счетов кажется решённой задачей: выбираешь валюту, умножаешь на обменный курс, печатаешь итог. На практике это одна из самых подверженных ошибкам областей любой биллинговой системы, и ошибки обходятся дорого, потому что всплывают в вашей дебиторской задолженности, налоговых декларациях и почтовых ящиках клиентов. Если вы разработчик, встраивающий мультивалютное выставление счетов в SaaS-продукт, инструмент для фрилансеров, платформу агентства или B2B-маркетплейс, сложность не в умножении. Сложность в том, чтобы решить, какой обменный курс использовать, когда его зафиксировать и как его сохранить, чтобы счёт, выставленный в марте, всё ещё корректно сверялся при оплате в июле.

Это руководство разбирает инженерные решения, которые имеют значение, с работающим кодом, использующим API обменных курсов для получения, фиксации и хранения курсов. Фокус — именно на валютном (FX) слое, той части, которую большинство руководств по выставлению счетов пропускают.

Почему мультивалютное выставление счетов — это больше, чем конвертация валют

Счёт в одной валюте — это моментальный снимок: количество на цену плюс налог. Мультивалютный счёт — это договор о моменте времени. Когда вы выставляете клиенту счёт в EUR, а учёт ведёте в USD, вы регистрируете дебиторскую задолженность, чья стоимость в USD фиксируется в день выставления, — даже если рыночный курс продолжает двигаться до тех пор, пока клиент действительно не заплатит.

Этот разрыв порождает три конкретные проблемы, которые простая конвертация игнорирует:

  1. Какой курс применяется. Курс на дату счёта, на дату платежа или «сегодняшний»? Они почти никогда не совпадают, а бухгалтерские стандарты (и GAAP, и IFRS) дают чёткий ответ.
  2. Проверяемость. Вам нужно уметь доказать спустя месяцы, какой именно курс вы использовали и откуда он взялся. «Мы взяли его из API» недостаточно, если вы не можете воспроизвести число.
  3. Курсовая прибыль и убыток. Разница между стоимостью на дату счёта и стоимостью на дату платежа — это реальная прибыль или убыток, которые должны где-то отразиться в вашей главной книге.

Сделайте это правильно — и мультивалютное выставление счетов станет скучным в лучшем смысле. Сделайте неправильно — и ваша финансовая команда проведёт последнюю неделю каждого квартала в погоне за копейками.

Золотое правило: фиксируйте обменный курс на дату счёта

Самое важное правило мультивалютного выставления счетов таково: заморозьте обменный курс в момент выставления счёта и никогда не пересчитывайте его.

И GAAP, и IFRS требуют регистрировать операцию в иностранной валюте по спот-курсу, действующему на дату операции. Для счёта дата операции — это дата выставления. Если вы выставляете клиенту счёт 1 июля, но ваша система обрабатывает его лишь 5 июля, вы всё равно используете курс на 1 июля. Это не правило Finexly и не предпочтение — так юридически устанавливается стоимость дебиторской задолженности в вашей функциональной (домашней) валюте.

Распространённый антипаттерн — показывать «живой» конвертированный итог, который меняется каждый раз, когда клиент обновляет счёт. Никогда так не делайте. Счёт — это фиксированное требование на определённую сумму. Клиент должен сумму в валюте счёта, а ваш учёт должен проводку по зафиксированному курсу. Рынок после этого может делать что угодно.

Практический вывод: конвертация валюты для выставления счетов — это операция однократной записи. Вы получаете курс один раз, сохраняете его вместе со счётом и считаете неизменным на всём протяжении жизни этого документа.

Как выбрать API обменных курсов для выставления счетов

Не всякий источник данных FX подходит для выставления счетов. Для биллинга вам нужны:

  • Покрытие каждой валюты, в которой вы выставляете счета (Finexly охватывает более 170 валют).
  • Надёжная метка времени «по состоянию на» на каждом курсе, чтобы вы могли доказать, какую котировку использовали.
  • Исторические курсы по дате, потому что счета задним числом и с исправлениями неизбежны.
  • Предсказуемые лимиты запросов и цены, чтобы всплеск объёма счетов не сломал биллинг. Изучите тарифные планы, прежде чем встраивать зависимость в путь оплаты.

Базовый запрос текущего курса выглядит так:

curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR&access_key=YOUR_API_KEY"

А ответ даёт вам курс плюс метку времени, которую вы сохраните рядом:

{
  "success": true,
  "base": "USD",
  "timestamp": 1753660800,
  "rates": {
    "EUR": 0.9213
  }
}

Если хотите изучить курсы в интерактивном режиме, прежде чем что-либо подключать, конвертер валют — быстрая проверка. Для более широкого взгляда на то, как Finexly сравнивается с другими поставщиками, см. сравнение валютных API.

Получение и фиксация курса счёта

Вот небольшой Python-помощник, который получает курс для счёта и возвращает всё, что нужно сохранить: курс, метку времени источника и конвертированный итог. Обратите внимание, что он возвращает курс и метаданные — а не просто число.

import os
import time
import requests
from decimal import Decimal, ROUND_HALF_UP

API_KEY = os.environ["FINEXLY_API_KEY"]
BASE_URL = "https://api.finexly.com/v1/latest"

def lock_invoice_rate(home_currency, invoice_currency):
    """Fetch and lock the FX rate to convert an invoice total
    (in invoice_currency) back into home_currency for the books."""
    resp = requests.get(BASE_URL, params={
        "base": invoice_currency,
        "symbols": home_currency,
        "access_key": API_KEY,
    }, timeout=10)
    resp.raise_for_status()
    data = resp.json()
    if not data.get("success"):
        raise RuntimeError("Rate lookup failed")

    rate = Decimal(str(data["rates"][home_currency]))
    return {
        "rate": rate,                       # invoice_currency -> home_currency
        "rate_base": invoice_currency,
        "rate_quote": home_currency,
        "source": "finexly",
        "as_of": data["timestamp"],         # store the source timestamp
        "locked_at": int(time.time()),      # when WE locked it
    }

def home_value(amount_invoice_ccy, rate):
    """Convert an invoice-currency amount into home currency."""
    return (Decimal(str(amount_invoice_ccy)) * rate).quantize(
        Decimal("0.01"), rounding=ROUND_HALF_UP
    )

Ключевая деталь в том, что lock_invoice_rate выполняется один раз, при создании счёта, и его результат записывается в запись счёта. Для промышленных вопросов вроде кэширования, повторных попыток и корректной обработки ответов 429 следуйте шаблонам из нашего руководства по кэшированию и обработке ошибок — вы не хотите, чтобы кратковременный сбой API заблокировал выставление счетов.

Хранение обменного курса вместе со счётом

Поскольку курс неизменен для каждого счёта, храните его на самом счёте, а не в общей таблице «текущий курс», которую вы можете перезаписать. Минимальная схема выглядит так:

CREATE TABLE invoices (
  id              BIGSERIAL PRIMARY KEY,
  issue_date      DATE        NOT NULL,
  invoice_ccy     CHAR(3)     NOT NULL,   -- what the customer is billed in
  home_ccy        CHAR(3)     NOT NULL,   -- your functional currency
  total_invoice   NUMERIC(18,2) NOT NULL, -- total in invoice_ccy
  fx_rate         NUMERIC(18,8) NOT NULL, -- invoice_ccy -> home_ccy, LOCKED
  fx_source       TEXT        NOT NULL,   -- e.g. 'finexly'
  fx_as_of        TIMESTAMPTZ NOT NULL,   -- the rate's source timestamp
  total_home      NUMERIC(18,2) NOT NULL  -- total_invoice * fx_rate, at issue
);

Три вещи делают эту схему удобной для аудита. Во-первых, fx_rate использует восемь знаков после запятой — курсам нужно гораздо больше точности, чем двум знакам денежной суммы, а раннее усечение вносит дрейф округления. Во-вторых, fx_as_of фиксирует, откуда взялось число и когда, так что любой аудитор сможет воспроизвести его по историческим данным. В-третьих, total_home вычисляется и сохраняется в момент выставления, поэтому отчёту, запущенному через полгода, не придётся ничего угадывать.

Ради прозрачности для клиента печатайте зафиксированный курс на самом счёте: сумму к оплате, валюту, использованный курс и дату его применения. Это широко рекомендуемая передовая практика именно потому, что она устраняет двусмысленность, когда платёж приходит по другому курсу.

Счета задним числом: используйте исторические курсы, а не сегодняшний

Рано или поздно вы выставите счёт с датой в прошлом — исправление, поздняя проводка или договор, указывающий более раннюю дату вступления в силу. Использовать сегодняшний курс для мартовского счёта попросту неверно, и это не пройдёт аудит. Вместо этого получите курс на фактическую дату счёта из исторического эндпоинта:

def lock_historical_rate(home_currency, invoice_currency, invoice_date):
    """invoice_date as 'YYYY-MM-DD'. Returns the locked rate for a
    backdated or corrected invoice."""
    resp = requests.get("https://api.finexly.com/v1/historical", params={
        "date": invoice_date,
        "base": invoice_currency,
        "symbols": home_currency,
        "access_key": API_KEY,
    }, timeout=10)
    resp.raise_for_status()
    data = resp.json()
    rate = Decimal(str(data["rates"][home_currency]))
    return {"rate": rate, "as_of": invoice_date, "source": "finexly"}

Правило идентично живому случаю — зафиксировать один раз, хранить вечно — но дата, которую вы запрашиваете, — это дата выставления счёта, а не текущий день. Более глубокий разбор работы с датированными курсами см. в руководстве по API исторических обменных курсов.

Округление и точность, сделанные правильно

Ошибки с деньгами почти всегда — ошибки округления. Два правила уберегут вас от неприятностей.

Никогда не используйте числа с плавающей точкой для денег. 0.1 + 0.2 не равно 0.3 в арифметике с плавающей точкой, и эти крошечные ошибки накапливаются по строкам счёта. Используйте десятичный тип: Decimal в Python, BigDecimal в Java, decimal в C# или представление в целых центах в JavaScript.

Решите, где происходит округление. Округляйте курс до полной точности (8+ знаков), но округляйте суммы до наименьшей единицы валюты — два знака для USD или EUR, ноль знаков для JPY или KRW, три для некоторых других. Частая ошибка — жёстко закодировать два знака и выставить счёт на ¥1,234.56, что не является допустимой суммой в иенах. Определяйте число знаков по валюте, используя данные о наименьшей единице из ISO 4217.

Для счетов со строками позиций предпочтительно округлять каждую строку, а затем суммировать, и сверять с округлённым итогом, чтобы напечатанные строки складывались в напечатанный итог. Какое бы соглашение вы ни выбрали, применяйте его последовательно в выставлении счетов, платежах и отчётности.

Признание курсовой прибыли и убытка в момент платежа

Вот где зафиксированный курс окупается. Когда клиент платит, вы конвертируете то, что фактически поступило на банковский счёт, обратно в домашнюю валюту по курсу даты платежа. Разница между этим и домашней стоимостью на дату счёта — это реализованная курсовая прибыль или убыток.

// Amounts kept as Decimal-like strings; use a money library in production.
function realizedFxGainLoss(invoice, paymentRate) {
  // invoice.totalInvoice: amount billed, in the invoice currency
  // invoice.fxRate:       LOCKED rate at issue (invoice_ccy -> home_ccy)
  // paymentRate:          rate on the day the payment settled

  const homeAtIssue   = invoice.totalInvoice * invoice.fxRate;
  const homeAtPayment = invoice.totalInvoice * paymentRate;

  const gainLoss = homeAtPayment - homeAtIssue;
  return {
    homeAtIssue:   round2(homeAtIssue),
    homeAtPayment: round2(homeAtPayment),
    fxGainLoss:    round2(gainLoss),        // > 0 gain, < 0 loss
  };
}

function round2(n) { return Math.round(n * 100) / 100; }

Вы получаете paymentRate так же, как фиксировали курс счёта, — вызовом latest на дату расчёта или вызовом historical, если сверяетесь постфактум. Проведите fxGainLoss на выделенный счёт «курсовая прибыль/убыток». Именно эта единственная проводка удерживает ваш учёт в равновесии, когда рынок движется между выставлением и платежом, и именно эту работу по сверке ручное мультивалютное выставление счетов делает неправильно. Компании, встраивающие это в автоматизированные конвейеры, могут опереться на ту же инфраструктуру курсов, описанную в нашем руководстве по мультивалютному SaaS-биллингу.

Кредит-ноты, возвраты и повторяющиеся счета

Три крайних случая завершают полноценную систему:

  • Кредит-ноты и возвраты должны сторнировать исходный счёт по его исходному зафиксированному курсу — а не по текущему. Возврат — это разворот исходной операции, поэтому повторно используйте fx_rate того счёта, который вы кредитуете. Любая остаточная разница на фактическую дату возврата становится ещё одной небольшой проводкой курсовой прибыли/убытка.
  • Повторяющиеся счета каждый получают собственный зафиксированный курс на собственную дату выставления. Подписка на 12 месяцев с ежемесячным выставлением порождает двенадцать счетов с двенадцатью курсами. Не фиксируйте единый курс на год, если только договор явно его не закрепляет.
  • Договорно зафиксированные курсы иногда переопределяют рынок — некоторые корпоративные договоры задают фиксированный курс на период. Поддержите поле ручного переопределения, но храните его с теми же метаданными аудита, чтобы оно оставалось воспроизводимым.

Часто задаваемые вопросы

Какой обменный курс использовать на счёте? Используйте спот-курс, действующий на дату выставления счёта, затем зафиксируйте его. И GAAP, и IFRS требуют регистрировать операции в иностранной валюте по курсу даты операции, а дата счёта — это и есть та дата. Не пересчитывайте его, когда клиент просматривает или оплачивает счёт.

Хранить обменный курс или только конвертированную сумму? Храните оба — плюс источник и метку времени курса. Хранение только конвертированного итога делает невозможным аудит или корректный расчёт курсовой прибыли/убытка позже. Курс, метка времени «по состоянию на» и источник вместе позволяют любому воспроизвести число.

Как обрабатывать счёт с датой в прошлом? Запросите исторический обменный курс на фактическую дату выставления вместо сегодняшнего. Правило зафиксировать один раз и хранить вечно то же самое; меняется лишь дата, которую вы запрашиваете.

Что вызывает курсовую прибыль или убыток по счёту? Движение рынка между датой счёта и датой платежа. Ваш учёт зарегистрировал дебиторскую задолженность по курсу даты счёта, но деньги приходят оценёнными по курсу даты платежа. Разница — реализованная курсовая прибыль или убыток, проводится на выделенный счёт главной книги.

Нужен ли платный валютный API, чтобы построить мультивалютное выставление счетов? Можно начать с бесплатного тарифа. Бесплатный API обменных курсов от Finexly охватывает живые и исторические курсы для объёмов на ранней стадии, а переходите на более высокий тариф только по мере роста пропускной способности счетов.

Начните сейчас

Мультивалютное выставление счетов сводится к одной дисциплине: зафиксируйте курс на дату счёта, сохраните его с полными метаданными аудита и сверьте разницу при платеже. Сделайте это — и международный биллинг перестанет быть авралом в конце квартала.

Готовы построить? Получите бесплатный ключ API Finexly — без кредитной карты. Начните с живых и исторических курсов для более чем 170 валют на бесплатном плане и масштабируйтесь по мере роста объёма счетов.

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 →