Currencies and Rates

7 views Markdown

The eight endpoints for the currencies, their rates and the rate provider.

Overview

An installation has one base currency, and the others carry their rate against it. Prices, invoices and reports all convert through that point.

Rates are either written by hand or come from a provider module. You pick the provider and set how often it runs, and a scheduled task refreshes the rates. An endpoint for pulling them by hand sits alongside.

A currency can be tied to countries: a visitor's country decides which one they see. A country belongs to one currency at a time.

Reference

Listing the Currencies

get/api/v1/admin/financial/currencies
Financial/GetCurrencies admin

Returns every currency on the installation with its rate.

Response fields data[] — 13
idintThe currency id.
codestringIts international code.
namestringThe currency name.
statusstringWhether it is live or off.
localboolWhether it is the installation's own. Every conversion runs through it.
hiddenboolWhether clients get to see it.
ratefloatIts rate against the installation's own.
formatintHow the number gets formatted.
prefixstringThe mark that goes before the amount.
suffixstringThe mark that goes after the amount.
countrystringIts default country.
countriesstring[]The countries tied to it. A visitor's country picks it.
modulesstring[]The payment methods working in it.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/financial/currencies' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/financial/currencies', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const { data } = await res.json();

const base = data.find((c) => c.local);
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currencies');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Rates run against the BASE currency; converting between two others passes through it.
$all  = Api::Financial()->GetCurrencies()['data'];
$base = current(array_filter($all, fn ($c) => $c['local']));

Reading the Rate Settings

get/api/v1/admin/financial/currency-settings
Financial/GetCurrencySettings admin

Returns which provider supplies the rates and how often they refresh.

Response fields data — 5
modulestringThe module supplying the rates.
auto_rate_enabledboolWhether the rates refresh by themselves.
update_periodstringHow often they refresh: hourly or daily.
last_run_atstring | nullWhen they last refreshed.
available_modulesstring[]The rate providers installed.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/financial/currency-settings' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/financial/currency-settings', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currency-settings');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// An OLD last-run time means stale rates; the refresh may have quietly stopped.
$s = Api::Financial()->GetCurrencySettings()['data'];

Writing the Rate Settings

put/api/v1/admin/financial/currency-settings
Financial/UpdateCurrencySettings admin

Changes the rate provider and how the refresh runs.

Body 4
modulestringThe rate provider to use.
auto_rate_enabledboolLets the rates refresh by themselves.
update_periodstringHow often they refresh. Daily by default.
module_dataobjectThe provider's own settings.
Response fields data — 5
dataobjectThe settings as they now stand. Same shape as the read endpoint.
Errors 2
module_not_found422No such provider.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/financial/currency-settings' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"module":"WAtlas","auto_rate_enabled":true,"update_period":"day"}'
const res = await fetch('https://panel.example.com/api/v1/admin/financial/currency-settings', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    module: 'WAtlas',
    auto_rate_enabled: true,
    update_period: 'day',
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currency-settings');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'module'            => 'WAtlas',
        'auto_rate_enabled' => true,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Put a provider through the test endpoint BEFORE switching; a bad key gives quiet staleness.
Api::Financial()->TestCurrencyModule(['module' => 'WAtlas']);
Api::Financial()->UpdateCurrencySettings(['module' => 'WAtlas']);

Testing the Provider

post/api/v1/admin/financial/currency-modules/test
Financial/TestCurrencyModule admin it saves nothing

Tests a rate provider by pulling a sample rate from it.

Body 2
modulestringreqThe provider to test.
module_dataobjectSettings to test with. They are not saved and serve this call alone.
Response fields data — 2
localstringThe base currency code.
ratesobjectThe sample rates the provider returned.
Errors 6
module_required422No provider name was given.
module_not_found422No such provider.
not_supported422The provider supplies no rates.
no_local422No base currency is set.
test_failed422The provider returned no rate at all.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/financial/currency-modules/test' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"module":"WAtlas"}'
const res = await fetch('https://panel.example.com/api/v1/admin/financial/currency-modules/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ module: 'WAtlas' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currency-modules/test');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['module' => 'WAtlas']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The test connects LIVE and saves nothing; a provider not yet chosen can be tried too.
$r = Api::Financial()->TestCurrencyModule([
    'module'      => 'WAtlas',
    'module_data' => ['WAtlas' => ['api_key' => $key]],
])['data'];

Refreshing the Rates

post/api/v1/admin/financial/currencies/sync
Financial/SyncCurrencyRates admin it runs on the spot

Pulls the rates from the provider by hand.

Body
No body is needed. The provider and the period come from the currency settings, not from the call, so send an empty body.
Response fields data — 3
rates_changedboolWhether the rates moved.
reasonstringWhy nothing moved. It comes back only when the refresh was skipped.
last_run_atstring | nullWhen the refresh happened.
Errors 2
sync_failed422The rates could not be pulled.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/financial/currencies/sync' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/financial/currencies/sync', {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const { data } = await res.json();
if (! data.rates_changed) console.warn(data.reason);
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currencies/sync');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A successful call does not mean the rates MOVED; read the reason field.
$r = Api::Financial()->SyncCurrencyRates()['data'];
if (! $r['rates_changed']) error_log($r['reason'] ?? 'skipped');

Reading One Currency

get/api/v1/admin/financial/currencies/{id}
Financial/GetCurrency admin

Returns a single currency.

Response fields data — 13
idintThe currency id.
codestringIts international code.
namestringThe currency name.
statusstringWhether it is live or off.
localboolWhether it is the installation's own. Every conversion runs through it.
hiddenboolWhether clients get to see it.
ratefloatIts rate against the installation's own.
formatintHow the number gets formatted.
prefixstringThe mark that goes before the amount.
suffixstringThe mark that goes after the amount.
countrystringIts default country.
countriesstring[]The countries tied to it. A visitor's country picks it.
modulesstring[]The payment methods working in it.
Errors 2
not_found404No such currency.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/financial/currencies/4' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch(`https://panel.example.com/api/v1/admin/financial/currencies/${id}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currencies/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The base currency's rate is ALWAYS one; trying to change it means nothing.
$c = Api::Financial()->GetCurrency(['id' => $id])['data'];

Updating a Currency

patch/api/v1/admin/financial/currencies/{id}
Financial/UpdateCurrency admin it can move the base

Changes a currency's look, its rate and what it is tied to.

Body 9
namestringThe currency name.
prefixstringThe mark that goes before the amount.
suffixstringThe mark that goes after the amount.
formatintHow the number gets formatted.
ratefloatIts rate against the base. With the refresh on, the next run writes over it.
hiddenboolKeeps it from clients.
countriesstring[]The countries to tie to it. A country can belong to one currency alone.
modulesstring[]The payment methods that work in it.
localboolMakes this the installation's base currency. The point every conversion rests on moves.
Response fields data — 13
dataobjectThe currency as it now stands. Same shape as the detail endpoint.
Errors 4
not_found404No such currency.
country_conflict422The country belongs to another currency.
currency_save_failed422The record could not be written.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/financial/currencies/4' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"US Dollar","prefix":"$","hidden":false}'
const res = await fetch(`https://panel.example.com/api/v1/admin/financial/currencies/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'US Dollar', prefix: '$', hidden: false }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currencies/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['name' => 'US Dollar', 'prefix' => '$']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Do not read the base field as ONE MORE switch: it moves the installation's money base
// and every rate is worked out again.
Api::Financial()->UpdateCurrency(['id' => $id, 'prefix' => '$']);

Switching a Currency On and Off

put/api/v1/admin/financial/currencies/{id}/status
Financial/SetCurrencyStatus admin

Decides whether a currency can be used at all.

Body 1
statusboolreqSwitches the currency on or off.
Response fields data — 13
dataobjectThe currency as it now stands. Same shape as the detail endpoint.
Errors 4
not_found404No such currency.
local_currency422The base currency cannot be switched off.
blocked_by_gate422A hook refused the change.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/financial/currencies/4/status' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":true}'
const res = await fetch(`https://panel.example.com/api/v1/admin/financial/currencies/${id}/status`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ status: true }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/financial/currencies/' . $id . '/status');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['status' => true]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Switching off leaves PAST invoices in that currency alone; it stops new choices.
Api::Financial()->SetCurrencyStatus(['id' => $id, 'status' => false]);

Pitfalls

Moving the base currency shifts everything

The base-currency field in the update body is no ordinary option. It moves the installation's money base, writes to the settings file and has every rate worked out again. Prices keep the same numbers while now meaning a different unit. Sending this field by accident is among the costliest mistakes here.

A rate written by hand goes at the next refresh

With the refresh on, writing a rate by hand is a passing correction: the provider writes over it on the next run. For a rate that holds, switch the refresh off first. This is why a rate seems to "come back" after being set by hand.

A successful refresh does not mean the rates moved

The refresh endpoint counts as successful even when it returns without pulling anything, and names the reason in a separate field. The provider may not have answered, the interval may not have passed, or the setting may be off. Monitoring that reads only the response code will not notice rates gone stale for months.

A country belongs to one currency

Tying a country to a currency while it sits on another gets the call refused. The country list replaces what was there, so a country left out drops its tie. When changing them, read the current list and add to it.

Test a provider before choosing it

The test endpoint opens a live connection and saves nothing. A provider not yet chosen can be tried with settings passed in. Switching provider without testing leaves the rates quietly unrefreshed. No error shows, and only the last-run time gives it away by standing still.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.