Code Examples

Go and the CurrencyCore API

Call /v1/convert and /v1/rates from Go with net/http, a context timeout, typed structs, and error handling.


There is no official Go SDK, so the net/http client below is the supported path. A Go client needs three things beyond the request: a context deadline, a bounded http.Client, and an error type that carries the API code. This example keeps all three so a caller can distinguish a bad key from a plan gate or a rate limit.

The example needs Go 1.20 or later. Free includes 1,000 requests per month at 60 per minute. Live keys require a paid plan; test keys use the same reference data and quota, capped at 60 per minute. Read the key from CURRENCYCORE_API_KEY, never from a flag default.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"time"
)

type convertResponse struct {
	From    string  `json:"from"`
	Amount  float64 `json:"amount"`
	Date    string  `json:"date"`
	Results []struct {
		To     string  `json:"to"`
		Result float64 `json:"result"`
		Rate   float64 `json:"rate"`
	} `json:"results"`
}

type ratesResponse struct {
	Base  string             `json:"base"`
	Date  string             `json:"date"`
	Rates map[string]float64 `json:"rates"`
}

type APIError struct {
	Status  int
	Code    string
	Message string
}

func (e *APIError) Error() string {
	return fmt.Sprintf("%s: %s (HTTP %d)", e.Code, e.Message, e.Status)
}

var client = &http.Client{Timeout: 10 * time.Second}

func getJSON(ctx context.Context, key, path string, out any) error {
	req, err := http.NewRequestWithContext(
		ctx, http.MethodGet, "https://api.currency-core.com/v1"+path, nil,
	)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+key)
	req.Header.Set("Accept", "application/json")

	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return err
	}

	if res.StatusCode < 200 || res.StatusCode >= 300 {
		var envelope struct {
			Error struct {
				Code    string `json:"code"`
				Message string `json:"message"`
			} `json:"error"`
		}
		if json.Unmarshal(body, &envelope) == nil && envelope.Error.Code != "" {
			return &APIError{
				Status:  res.StatusCode,
				Code:    envelope.Error.Code,
				Message: envelope.Error.Message,
			}
		}
		return &APIError{Status: res.StatusCode, Code: "http_error", Message: "unexpected response"}
	}

	return json.Unmarshal(body, out)
}

func main() {
	key := os.Getenv("CURRENCYCORE_API_KEY")
	if key == "" {
		log.Fatal("Set CURRENCYCORE_API_KEY")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	params := url.Values{"from": {"USD"}, "to": {"INR"}, "amount": {"100"}}
	var conversion convertResponse
	if err := getJSON(ctx, key, "/convert?"+params.Encode(), &conversion); err != nil {
		log.Fatal(err)
	}

	var rates ratesResponse
	if err := getJSON(ctx, key, "/rates/EUR", &rates); err != nil {
		log.Fatal(err)
	}

	fmt.Println(conversion.Results[0].Result, rates.Rates["USD"])
}

/v1/convert returns one entry per target; /v1/rates/{base} returns a map with the base at 1:

{
  "from": "USD",
  "amount": 100,
  "date": "2026-11-10",
  "results": [{ "to": "INR", "result": 8345.0, "rate": 83.45 }]
}
{ "base": "EUR", "date": "2026-11-10", "rates": { "EUR": 1, "USD": 1.0861 } }

Use errors.As to branch on *APIError: 400 is input, 401 is auth, 403 is a plan or history window, and 429 is a limit. Retry 429 and 5xx with backoff; do not retry the other 4xx responses.

Cache by date and base. Historical values are stable for normal caching, so a long TTL is reasonable. Keep the observation date. Today’s snapshot should use a TTL that matches how much staleness your product can tolerate.

Next steps