Reference Data
Best practices for downloading, caching, and refreshing reference data
Overview
Reference data — countries, currencies, categories, and subcategories — changes infrequently. Download it once after authentication and cache it locally. This avoids redundant API calls, reduces latency, and ensures your application works smoothly even during brief network disruptions.
What to Download
| Endpoint | Data | Changes |
|---|---|---|
GET /api/v1/countries | Country names, ISO codes, dialing prefixes | Rarely (new countries are exceptional) |
GET /api/v1/currencies | Currency names, ISO codes, decimal precision | Rarely (new currencies are uncommon) |
GET /api/v1/categories | Product category names | Occasionally (when new product verticals are added) |
GET /api/v1/subcategories | Product subcategory names | Occasionally |
All four endpoints return the full dataset in a single response — there is no pagination to handle.
Download Once at Startup
Fetch all reference data immediately after a successful login. This is a one-time cost of 4 API calls that gives your application everything it needs for mapping IDs to display names, validating user input, and building product filters.
type ReferenceData struct {
Countries []Country
Currencies []Currency
Categories []Category
SubCategories []SubCategory
FetchedAt time.Time
}
func FetchReferenceData(host, token string) (*ReferenceData, error) {
rd := &ReferenceData{FetchedAt: time.Now()}
// Fetch all four in parallel
g, _ := errgroup.WithContext(context.Background())
g.Go(func() error {
countries, err := fetchJSON[[]Country](host+"/api/v1/countries", token)
rd.Countries = countries
return err
})
g.Go(func() error {
currencies, err := fetchJSON[[]Currency](host+"/api/v1/currencies", token)
rd.Currencies = currencies
return err
})
g.Go(func() error {
categories, err := fetchJSON[[]Category](host+"/api/v1/categories", token)
rd.Categories = categories
return err
})
g.Go(func() error {
subcategories, err := fetchJSON[[]SubCategory](host+"/api/v1/subcategories", token)
rd.SubCategories = subcategories
return err
})
if err := g.Wait(); err != nil {
return nil, err
}
return rd, nil
}Build Lookup Maps
Raw arrays are fine for listing, but most operations need a lookup by ID. Build maps once after download:
type RefLookup struct {
CountryByID map[int]Country
CountryByAlpha2 map[string]Country
CurrencyByID map[int]Currency
CurrencyByCode map[string]Currency
CategoryByID map[int]Category
}
func BuildLookups(rd *ReferenceData) *RefLookup {
rl := &RefLookup{
CountryByID: make(map[int]Country, len(rd.Countries)),
CountryByAlpha2: make(map[string]Country, len(rd.Countries)),
CurrencyByID: make(map[int]Currency, len(rd.Currencies)),
CurrencyByCode: make(map[string]Currency, len(rd.Currencies)),
CategoryByID: make(map[int]Category, len(rd.Categories)),
}
for _, c := range rd.Countries {
rl.CountryByID[c.ID] = c
rl.CountryByAlpha2[c.Alpha2] = c
}
for _, c := range rd.Currencies {
rl.CurrencyByID[c.ID] = c
rl.CurrencyByCode[c.Currency] = c
}
for _, c := range rd.Categories {
rl.CategoryByID[c.ID] = c
}
return rl
}This lets you resolve a country_id from a product response into a full country name without an extra API call.
Refresh Schedule
Reference data is served with a 24-hour cache header and a 10-minute stale-while-revalidate window. Match this on your side:
| Strategy | When to use |
|---|---|
| Daily refresh | Long-running services — refresh once every 24 hours in a background goroutine |
| On restart | Short-lived processes — download fresh data each time the application starts |
| Manual trigger | Admin dashboards — add a "Refresh Reference Data" button for on-demand updates |
func (rd *ReferenceData) IsStale() bool {
return time.Since(rd.FetchedAt) > 24*time.Hour
}You do not need to poll these endpoints. A daily refresh is more than sufficient. The server returns cache headers (Cache-Control: public, max-age=86400) confirming the data is stable for 24 hours.
Local Storage
For applications that restart frequently (serverless functions, CLI tools, CI jobs), avoid re-downloading on every cold start by persisting reference data to disk:
func SaveToFile(rd *ReferenceData, path string) error {
data, err := json.Marshal(rd)
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}
func LoadFromFile(path string) (*ReferenceData, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var rd ReferenceData
if err := json.Unmarshal(data, &rd); err != nil {
return nil, err
}
// Re-download if stale
if rd.IsStale() {
return nil, fmt.Errorf("cached data is stale")
}
return &rd, nil
}Error Handling
Reference data endpoints can fail due to auth issues or transient errors. Handle this gracefully:
- On startup failure — retry with exponential backoff (3 attempts max). If all attempts fail, exit with a clear error. Your application cannot function without reference data.
- On refresh failure — log the error and continue with the existing cached data. Try again on the next refresh cycle.
- Feature-gated endpoints — categories and subcategories require the
vouchersfeature to be enabled for your client. If you receive a400 INVALID_FEATUREerror, skip those endpoints and proceed without them.
Checklist
- Download all reference data after login, in parallel
- Build ID and code lookup maps for fast access
- Refresh daily for long-running services, or on each restart for short-lived ones
- Persist to disk if your application has frequent cold starts
- Handle feature-gated endpoints (categories, subcategories) gracefully
- Never call reference endpoints per-request — always use your local cache