This guide builds a production-ready exchange rate API client in Golang using only the standard library. You’ll get typed response structs, request deadlines via context, a typed error for every failure mode, currency conversion and a cache that is safe to share across goroutines.
Setup
Get a key from the free sign-up and export it:
export FXAPI_KEY="your-api-key"
All endpoints are GET requests to https://api.fxapi.com/v1/... with the key in the apikey header. The response shape for latest, historical and convert is the same, so one struct covers all three.
Typed structs and errors
package fxapi
import (
"fmt"
"time"
)
type Rate struct {
Code string `json:"code"`
Value float64 `json:"value"`
}
type RatesResponse struct {
Meta struct {
LastUpdatedAt time.Time `json:"last_updated_at"`
} `json:"meta"`
Data map[string]Rate `json:"data"`
}
type APIError struct {
Status int
Msg string
Errors map[string]any // populated on 422, keyed by parameter name
}
func (e *APIError) Error() string { return fmt.Sprintf("fxapi: HTTP %d: %s", e.Status, e.Msg) }
An exchange rate API client in Go with net/http
package fxapi
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
const baseURL = "https://api.fxapi.com/v1"
type Client struct {
key string
http *http.Client
}
func NewClient(key string) *Client {
return &Client{key: key, http: &http.Client{Timeout: 15 * time.Second}}
}
func (c *Client) get(ctx context.Context, path string, q url.Values, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path+"?"+q.Encode(), nil)
if err != nil {
return err
}
req.Header.Set("apikey", c.key)
resp, err := c.http.Do(req)
if err != nil {
return err // network error or context deadline exceeded
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return json.NewDecoder(resp.Body).Decode(out)
}
apiErr := &APIError{Status: resp.StatusCode}
switch resp.StatusCode {
case http.StatusUnauthorized:
apiErr.Msg = "invalid API key"
case http.StatusForbidden:
apiErr.Msg = "endpoint or option not included in your plan"
case http.StatusUnprocessableEntity:
var body struct {
Errors map[string]any `json:"errors"`
}
_ = json.NewDecoder(resp.Body).Decode(&body)
apiErr.Msg, apiErr.Errors = "validation error", body.Errors
case http.StatusTooManyRequests:
apiErr.Msg = "rate limit reached, monthly quota left: " +
resp.Header.Get("X-RateLimit-Remaining-Quota-Month")
default:
apiErr.Msg = resp.Status
}
return apiErr
}
func (c *Client) Latest(ctx context.Context, base string, currencies ...string) (*RatesResponse, error) {
q := url.Values{"base_currency": {base}}
if len(currencies) > 0 {
q.Set("currencies", strings.Join(currencies, ","))
}
var r RatesResponse
return &r, c.get(ctx, "/latest", q, &r)
}
func (c *Client) Historical(ctx context.Context, day time.Time, base string, currencies ...string) (*RatesResponse, error) {
q := url.Values{"date": {day.Format("2006-01-02")}, "base_currency": {base}}
if len(currencies) > 0 {
q.Set("currencies", strings.Join(currencies, ","))
}
var r RatesResponse
return &r, c.get(ctx, "/historical", q, &r)
}
// Convert requires the Basic plan or higher (HTTP 403 otherwise).
func (c *Client) Convert(ctx context.Context, value float64, base string, currencies ...string) (*RatesResponse, error) {
q := url.Values{"value": {fmt.Sprint(value)}, "base_currency": {base}}
if len(currencies) > 0 {
q.Set("currencies", strings.Join(currencies, ","))
}
var r RatesResponse
return &r, c.get(ctx, "/convert", q, &r)
}
Every call takes a context.Context, so the caller decides how long to wait and cancellation propagates from incoming HTTP requests.
Latest and historical exchange rates in Go
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"example.com/yourapp/fxapi"
)
func main() {
client := fxapi.NewClient(os.Getenv("FXAPI_KEY"))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
latest, err := client.Latest(ctx, "EUR", "USD", "GBP", "JPY")
var apiErr *fxapi.APIError
if errors.As(err, &apiErr) && apiErr.Status == 429 {
log.Fatal("quota reached – serve cached rates instead")
} else if err != nil {
log.Fatal(err)
}
fmt.Println(latest.Meta.LastUpdatedAt, latest.Data["USD"].Value)
day := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
hist, err := client.Historical(ctx, day, "USD", "EUR")
if err != nil {
log.Fatal(err)
}
fmt.Println(hist.Data["EUR"].Value)
}
Historical values are end-of-day rates in UTC for any date from 1999-01-01 to yesterday. Need a series instead of single days? /v1/range returns daily, hourly or minute data on the Professional plan – see the time series API.
Currency conversion
On Basic and higher plans, client.Convert(ctx, 250, "USD", "EUR") returns the converted amount in Data["EUR"].Value. On the Free plan, or to save requests, convert locally from cached rates. Keep money in integer minor units:
// amountCents in the base currency, rate = target units per 1 base unit.
func convertMinor(amountCents int64, rate float64, targetDigits int) int64 {
scale := math.Pow10(targetDigits)
return int64(math.Round(float64(amountCents) / 100 * rate * scale))
}
targetDigits comes from the decimal_digits field of /v1/currencies – 2 for EUR, 0 for JPY, 3 for KWD. For accounting-grade arithmetic use a decimal package; the rounding guide explains why.
A concurrency-safe cache
Latest rates only change as often as your plan updates them: daily on Free, hourly on Basic, every 60 seconds on Professional and Enterprise (pricing). Cache one all-currency response and serve every goroutine from it:
type RateCache struct {
client *fxapi.Client
ttl time.Duration
mu sync.RWMutex
data *fxapi.RatesResponse
fetchedAt time.Time
}
func (c *RateCache) Get(ctx context.Context) (*fxapi.RatesResponse, error) {
c.mu.RLock()
if c.data != nil && time.Since(c.fetchedAt) < c.ttl {
defer c.mu.RUnlock()
return c.data, nil
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
if c.data != nil && time.Since(c.fetchedAt) < c.ttl {
return c.data, nil // another goroutine refreshed it
}
fresh, err := c.client.Latest(ctx, "USD") // all currencies, one request
if err != nil {
if c.data != nil {
return c.data, nil // serve stale rates on 429 or network errors
}
return nil, err
}
c.data, c.fetchedAt = fresh, time.Now()
return c.data, nil
}
The double-checked lock ensures only one goroutine refreshes at a time. Cross rates between any two currencies are Data[to].Value / Data[from].Value against the shared USD base. For multi-instance deployments, store the response in Redis – see caching exchange rates.
Using the official Go module
github.com/everapihq/fxapi-go exposes package-level functions that take a map[string]string of parameters and return the raw response body as []byte:
go get github.com/everapihq/fxapi-go
import fxapi "github.com/everapihq/fxapi-go"
fxapi.Init(os.Getenv("FXAPI_KEY"))
body := fxapi.Latest(map[string]string{"base_currency": "USD", "currencies": "EUR"})
var r RatesResponse
if err := json.Unmarshal(body, &r); err != nil {
log.Fatal(err)
}
The functions don’t return errors or accept a context, and the current version sends the parameter map in the request body rather than the query string – check that base_currency and currencies are applied before relying on it. For production services with timeouts and status-code handling, the net/http client above gives you more control.
Next steps
- API documentation and the OpenAPI spec for code generation (for example with
oapi-codegen). - Check your remaining quota with
/v1/status– free to call. - Payments and wallets: fintech payments use case.
The examples in this guide use fxapi, a foreign exchange rates API with a free plan of 300 requests a month – no credit card required.
Frequently asked questions
Do I need a third-party library to call the exchange rate API from Go?
net/http and encoding/json from the standard library are all you need. The examples on this page have no external dependencies.How should I represent money in Go?
float64. Use integer minor units (int64 cents) or a decimal package for amounts, and round once per currency’s decimal_digits. See currency rounding and precision.How do I set a timeout for exchange rate requests in Go?
http.NewRequestWithContext and a context.WithTimeout, and also set http.Client.Timeout as a backstop. Never use http.DefaultClient without a timeout in production.Can I parse last_updated_at as time.Time?
2026-10-01T23:59:59Z), which encoding/json decodes directly into a time.Time field.Is there an official Go module?
github.com/everapihq/fxapi-go. It returns raw JSON bytes, so you still unmarshal into your own structs.