Acumatica already knows how to do multi-currency accounting — it has Currency Rates, rate types, and the whole revaluation machinery. What it does not have is a live feed. Someone types today's rates in by hand, or they go stale. Wiring Currencycloud (or any FX provider) into the Currency Rates table is a small integration with a few sharp edges: mapping their quote to Acumatica's rate direction, writing rates idempotently so a re-run does not create duplicates, and making a failed pull loud instead of silently leaving yesterday's rate in place. This is how I build that pull.
How Acumatica stores a rate
Currency rates live in the CurrencyRate DAC, keyed by from-currency, to-currency, a rate type (CurrencyRateType — e.g. SPOT, or your own), and an effective date. The two fields that trip people up are CuryRate and RateReciprocal: Acumatica stores the rate in one direction and the reciprocal for the other, and it applies them according to the rate type's configuration. If you write the rate the wrong way round, everything reconciles to a mirror-image number and nobody notices until a month-end revaluation looks wrong. Decide, per rate type, exactly which direction the provider quotes and which direction Acumatica expects, and pin it down before writing a single row.
Create a rate type like CCLOUD in Currency Rate Types rather than overwriting SPOT. It keeps machine-fed rates auditable and separate from anything a controller entered by hand, and it lets you point specific customers or GL processes at the feed deliberately.
Fetching from Currencycloud
Currencycloud's REST API uses a login-token model: you authenticate with a login ID and API key to get a short-lived auth token, then pass it as a header on subsequent calls. Rates come from their rates endpoint for a currency pair. Wrap it in a small typed client; do not scatter HttpClient calls through your graph.
public sealed class CurrencycloudClient
{
private readonly HttpClient _http;
private string _authToken;
public CurrencycloudClient(HttpClient http) => _http = http;
public async Task AuthenticateAsync(string loginId, string apiKey)
{
var resp = await _http.PostAsync("/v2/authenticate/api",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["login_id"] = loginId,
["api_key"] = apiKey,
}));
resp.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
_authToken = json.RootElement.GetProperty("auth_token").GetString();
}
// Returns the mid rate for BUY/SELL of the pair, e.g. "USDKES"
public async Task<decimal> GetRateAsync(string pair)
{
using var req = new HttpRequestMessage(HttpMethod.Get,
$"/v2/rates/find?currency_pair={pair}");
req.Headers.Add("X-Auth-Token", _authToken);
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
// rates node holds [bid, offer]; take the mid for a book rate
var rates = json.RootElement.GetProperty("rates")
.GetProperty(pair);
var bid = rates[0].GetDecimal();
var offer = rates[1].GetDecimal();
return (bid + offer) / 2m;
}
}
Whether you book the mid, the bid, or the offer is a business decision, not a technical one — confirm it with finance. What matters technically is that you extract a single, well-defined number per pair and know which side of the quote it is.
Writing the rate idempotently
The pull runs on a schedule and will re-run — after a transient failure, on a manual retry, or because someone triggers it twice. Writing must be idempotent: one rate per pair per rate type per effective date, updated in place, never duplicated. Locate the existing row first and update it; only insert when none exists.
public void UpsertRate(PXGraph graph, string fromCury, string toCury,
string rateTypeId, DateTime effDate, decimal rate)
{
var existing = PXSelect<CurrencyRate,
Where<CurrencyRate.fromCuryID, Equal<Required<CurrencyRate.fromCuryID>>,
And<CurrencyRate.toCuryID, Equal<Required<CurrencyRate.toCuryID>>,
And<CurrencyRate.curyRateType, Equal<Required<CurrencyRate.curyRateType>>,
And<CurrencyRate.curyEffDate, Equal<Required<CurrencyRate.curyEffDate>>>>>>
.Select(graph, fromCury, toCury, rateTypeId, effDate)
.FirstOrDefault()?.GetItem<CurrencyRate>();
var cache = graph.Caches[typeof(CurrencyRate)];
var row = existing ?? new CurrencyRate
{
FromCuryID = fromCury,
ToCuryID = toCury,
CuryRateType = rateTypeId,
CuryEffDate = effDate,
};
row.CuryRate = rate;
row.RateReciprocal = Math.Round(1m / rate, 8);
cache.Update(existing == null ? cache.Insert(row) : row);
graph.Actions.PressSave();
}
Compute RateReciprocal from the rate every time rather than pulling a separate reciprocal quote from the provider. If the two come from different fetches they can disagree by rounding, and a self-inconsistent rate row produces revaluation differences that are almost impossible to trace back.
Scheduling and failing loudly
Run the pull as an Acumatica processing screen backed by a scheduled Automation Schedule, or from an external worker hitting the REST endpoint — either is fine. The rule that matters is the failure behavior: a stale rate is worse than an obvious error, because it silently mis-prices every foreign-currency transaction booked that day. If the provider is unreachable or returns a rate outside a sanity band, do not write anything and raise a visible failure — a failed schedule, an email, a Business Event — so a human knows to enter rates manually.
An FX API can return a zero, a mis-decimalled value, or an inverted pair during an incident. Reject anything that deviates from yesterday's rate beyond a threshold you agree with finance (say 10 percent) and fail the run instead of booking it. One bad rate written to CurrencyRate can silently corrupt a day of postings.
Testing without the live API
The two failure modes worth automated coverage are direction and idempotency, and neither needs the real provider. Put the client behind an interface, feed a canned quote, and assert two things: the rate lands in the correct direction with a consistent reciprocal, and running the upsert twice for the same pair/date leaves exactly one row with the same value. Those two tests catch the bugs that actually reach production — everything else is standard HTTP plumbing.
Wrapping up
An FX feed into Acumatica is a small integration whose whole value is that it runs unattended and correctly. Give the machine feed its own rate type, extract one well-defined number per pair, write it idempotently with a computed reciprocal, sanity-bound it, and make failure loud. Get those right and month-end revaluation stops being a place where mystery differences appear.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.