On this page:
10.1 Configuration Parameters
current-bc-tenant
current-bc-environment
current-bc-company
current-bc-client-id
current-bc-client-secret
current-bc-redirect-uri
10.2 Behavior Parameters
current-bc-auto-paginate
current-bc-log-requests
current-bc-log-level
10.3 Response Caching
10.3.1 Cache Parameters
current-bc-cache-dir
current-bc-cache-mode
10.3.2 Entity-Based Cache API
bc-cache-put!
bc-cache-get
bc-cache-remove!
10.3.3 Cache Utilities
cache-list
cache-clear!
10.3.4 Cache File Format
10.4 Authentication
bc-authenticate!
bc-authenticate/  interactive!
bc-refresh-token!
bc-authenticate/  cached!
current-bc-token
token-valid?
token-expired?
bc-token
10.5 Result Wrapper
bc-result
bc-result/  c
make-bc-result
10.5.1 Result Accessors
bc-result-count
bc-result-ref
bc-result-first
bc-result->list
bc-result-size
bc-result-empty?
in-bc-result
10.6 Exceptions
exn:  fail:  bcnav
exn:  fail:  bcnav:  auth
exn:  fail:  bcnav:  http
exn:  fail:  bcnav:  rate-limit
10.7 Logging
start-bc-listener
stop-bc-listener
log-bc-debug
log-bc-info
log-bc-warning
log-bc-error
10.8 Admin Center API
10.8.1 Configuration
current-bc-admin-api-version
10.8.2 Environment Discovery
bc-environment
bc-environment/  c
environments-list
9.3

10 Core Module Reference🔗

 (require bcnav) package: bcnav-lib

The main bcnav module provides authentication, configuration, and re-exports the OData query DSL.

10.1 Configuration Parameters🔗

These parameters control bcnav’s connection to Business Central. Set them before authenticating.

parameter

(current-bc-tenant)  (or/c #f string?)

(current-bc-tenant tenant)  void?
  tenant : (or/c #f string?)
 = #f
The Azure AD tenant identifier. This can be:
  • A tenant GUID: "12345678-1234-1234-1234-123456789012"

  • A domain name: "contoso.onmicrosoft.com"

Required for authentication.

parameter

(current-bc-environment)  string?

(current-bc-environment env)  void?
  env : string?
 = "Production"
The Business Central environment name. Common values are "Production" and "Sandbox". Check your BC admin center for available environments.

parameter

(current-bc-company)  (or/c #f string?)

(current-bc-company company)  void?
  company : (or/c #f string?)
 = #f
The BC company GUID to access. This is a GUID, not the company name. Required for most API operations.

parameter

(current-bc-client-id)  (or/c #f string?)

(current-bc-client-id client-id)  void?
  client-id : (or/c #f string?)
 = #f
Your Azure AD app registration’s Application (client) ID. Required for authentication.

parameter

(current-bc-client-secret)  (or/c #f string?)

(current-bc-client-secret secret)  void?
  secret : (or/c #f string?)
 = #f
Your Azure AD app registration’s client secret. Required for client credentials authentication (bc-authenticate!). Not needed for interactive authentication.

parameter

(current-bc-redirect-uri)  string?

(current-bc-redirect-uri uri)  void?
  uri : string?
 = "http://localhost:8080/oauth/authorization"
The redirect URI for interactive authentication. Must match a redirect URI configured in your Azure AD app registration.

10.2 Behavior Parameters🔗

parameter

(current-bc-auto-paginate)  boolean?

(current-bc-auto-paginate paginate?)  void?
  paginate? : boolean?
 = #t
When #t, list functions (like customers) automatically follow @odata.nextLink to retrieve all pages of results. When #f, only the first page is returned.

parameter

(current-bc-log-requests)  boolean?

(current-bc-log-requests log?)  void?
  log? : boolean?
 = #f
When #t, HTTP requests and responses are logged at the debug level. Useful for troubleshooting.

parameter

(current-bc-log-level)  (or/c 'debug 'info 'warning 'error)

(current-bc-log-level level)  void?
  level : (or/c 'debug 'info 'warning 'error)
 = 'info
Controls the minimum severity of log messages produced by bcnav.

10.3 Response Caching🔗

bcnav provides file-based caching for API responses. This is useful for:
  • Speeding up development by avoiding repeated API calls

  • Creating test fixtures with real data

  • Working offline with previously cached data

10.3.1 Cache Parameters🔗

parameter

(current-bc-cache-dir)  (or/c #f path-string?)

(current-bc-cache-dir dir)  void?
  dir : (or/c #f path-string?)
 = #f
The directory where cache files are stored. Set to #f to disable caching entirely.

When set, the cache is always consulted before making GET requests—if matching cached data exists, it is returned instead of calling the API.

Path resolution: Relative paths are resolved against current-directory at the time of each cache operation. If you change directories during a session, cache lookups will use the new working directory. Use an absolute path if you need consistent behavior regardless of working directory.

Directory creation: When writing to the cache, the directory is created automatically (including parent directories) if it doesn’t exist.

Disk-only: The cache is purely file-based with no in-memory retention. Each cache lookup reads from disk. This means cached data survives program restarts, but there’s no speed benefit from repeated lookups within a session beyond filesystem caching.

;; Relative path (resolved against current-directory)
(current-bc-cache-dir "bc-cache")
 
;; Absolute path (recommended for consistency)
(current-bc-cache-dir "/path/to/cache")
 
;; Absolute path based on current location
(current-bc-cache-dir (path->string
                        (build-path (current-directory) "bc-cache")))

parameter

(current-bc-cache-mode)  (or/c 'manual 'all)

(current-bc-cache-mode mode)  void?
  mode : (or/c 'manual 'all)
 = 'manual
Controls whether API responses are automatically saved to the cache.

  • 'manual (default): Responses are never auto-saved. Use bc-cache-put! to manually populate the cache.

  • 'all: Every GET response is automatically saved to the cache.

In both modes, cached data is always used when available. The mode only affects whether new responses are written.

10.3.2 Entity-Based Cache API🔗

The primary cache API works with bc-entity structs, using the current configuration (current-bc-tenant, current-bc-environment, current-bc-company) to construct cache keys automatically.

procedure

(bc-cache-put! entity data)  void?

  entity : bc-entity?
  data : any/c
(bc-cache-put! entity data query)  void?
  entity : bc-entity?
  data : any/c
  query : query?
(bc-cache-put! entity id data)  void?
  entity : bc-entity?
  id : string?
  data : any/c
Stores data in the cache for an entity.

;; Cache all customers
(bc-cache-put! customers
               '#hasheq((value . (#hasheq((id . "abc") (name . "Acme"))
                                  #hasheq((id . "def") (name . "Beta"))))))
 
;; Cache filtered results
(define active-query (make-query #:filter (= 'status "Active")))
(bc-cache-put! customers active-data active-query)
 
;; Cache a single customer
(bc-cache-put! customers "abc-123-def"
               '#hasheq((id . "abc-123-def")
                        (displayName . "Acme Corp")))

procedure

(bc-cache-get entity)  (or/c #f any/c)

  entity : bc-entity?
(bc-cache-get entity query-or-id)  (or/c #f any/c)
  entity : bc-entity?
  query-or-id : (or/c query? string?)
Retrieves cached data for an entity. Returns #f if no cached data exists.

  • (bc-cache-get entity) Get cached list endpoint response

  • (bc-cache-get entity query) Get cached filtered list response

  • (bc-cache-get entity id) Get cached single record by ID

procedure

(bc-cache-remove! entity)  void?

  entity : bc-entity?
(bc-cache-remove! entity query-or-id)  void?
  entity : bc-entity?
  query-or-id : (or/c query? string?)
Removes cached data for an entity.

10.3.3 Cache Utilities🔗

procedure

(cache-list)  (listof hash?)

Returns a list of all cache entries with metadata. Each entry is a hash containing:
  • 'url The cached URL

  • 'query Query parameters (list of pairs)

  • 'cached-at Unix timestamp when cached

  • 'cache-file Path to the cache file

  • 'response The cached response data

Useful for inspecting what’s in the cache.

procedure

(cache-clear!)  void?

Removes all cache files from current-bc-cache-dir.

10.3.4 Cache File Format🔗

Cache files are ".rktd" files (Racket data files) that can be read with read. Each file contains a hash with the structure:

'#hasheq((url . "https://api.businesscentral.dynamics.com/...")
         (query)
         (cached-at . 1702900000)
         (response . #hasheq((value . (...)))))

You can manually create or edit these files to populate the cache with test fixtures. The filename is a SHA-1 hash of the URL and query parameters.

10.4 Authentication🔗

procedure

(bc-authenticate! [#:tenant tenant    
  #:client-id client-id    
  #:client-secret client-secret])  bc-token?
  tenant : (or/c #f string?) = (current-bc-tenant)
  client-id : (or/c #f string?) = (current-bc-client-id)
  client-secret : (or/c #f string?) = (current-bc-client-secret)
Authenticates using the OAuth 2.0 client credentials flow. This is appropriate for service-to-service communication where no user context is needed.

On success, stores the token in current-bc-token and returns it.

Raises exn:fail:bcnav:auth if authentication fails.

(bc-authenticate!)
;; or with explicit credentials:
(bc-authenticate! #:tenant "contoso.onmicrosoft.com"
                  #:client-id "abc123-..."
                  #:client-secret "secret...")

procedure

(bc-authenticate/interactive! [#:tenant tenant 
  #:client-id client-id 
  #:redirect-uri redirect-uri 
  #:scopes scopes]) 
  bc-token?
  tenant : (or/c #f string?) = (current-bc-tenant)
  client-id : (or/c #f string?) = (current-bc-client-id)
  redirect-uri : string? = (current-bc-redirect-uri)
  scopes : (listof string?)
   = '("https://api.businesscentral.dynamics.com/.default")
Authenticates using the OAuth 2.0 authorization code flow. Opens a browser for the user to sign in and grant consent.

This flow provides a refresh token, allowing automatic token refresh without user interaction.

On success, stores the token in current-bc-token and returns it.

procedure

(bc-refresh-token! [#:tenant tenant    
  #:client-id client-id    
  #:client-secret client-secret])  bc-token?
  tenant : (or/c #f string?) = (current-bc-tenant)
  client-id : (or/c #f string?) = (current-bc-client-id)
  client-secret : (or/c #f string?) = #f
Refreshes the current token using its refresh token. Only works if the current token has a refresh token (i.e., was obtained via bc-authenticate/interactive!).

Client credentials tokens cannot be refreshed; call bc-authenticate! again instead.

procedure

(bc-authenticate/cached! [#:tenant tenant    
  #:client-id client-id    
  #:redirect-uri redirect-uri    
  #:port port    
  #:force-refresh force-refresh])  void?
  tenant : (or/c #f string?) = (current-bc-tenant)
  client-id : (or/c #f string?) = (current-bc-client-id)
  redirect-uri : string? = (current-bc-redirect-uri)
  port : exact-positive-integer? = 8080
  force-refresh : boolean? = #f
Authenticates interactively with automatic token caching using OS-native secure storage.

This is the recommended authentication function for interactive use. It:
  • Loads any cached token from secure storage

  • Uses the cached token if still valid

  • Automatically refreshes an expired token if it has a refresh token

  • Falls back to interactive browser authentication if needed

  • Saves the new token to secure storage after authentication

Platform support:
  • Windows: Uses DPAPI (Data Protection API) with user-scoped encryption

  • macOS: Uses the login Keychain via the security CLI

  • Linux: Not supported; raises an error with guidance for manual token management

The #:port parameter specifies which port the local OAuth callback server listens on. This must match your Azure AD app registration’s redirect URI.

Set #:force-refresh to #t to ignore any cached token and force a new interactive authentication.

See Appendix: Security for details on how tokens are stored securely.

;; Typical usage - handles caching automatically
(bc-authenticate/cached!)
 
;; Force re-authentication even if cached token is valid
(bc-authenticate/cached! #:force-refresh #t)
 
;; With explicit parameters
(bc-authenticate/cached! #:tenant "contoso.onmicrosoft.com"
                         #:client-id "abc123-..."
                         #:port 8080)

parameter

(current-bc-token)  (or/c #f bc-token?)

(current-bc-token token)  void?
  token : (or/c #f bc-token?)
 = #f
The current authentication token. Set automatically by authentication functions. You generally don’t need to access this directly.

procedure

(token-valid?)  boolean?

Returns #t if there is a current token that has not expired.

procedure

(token-expired?)  boolean?

Returns #t if the current token exists but has expired (or will expire within 60 seconds).

struct

(struct bc-token (access-token expires-at refresh-token)
    #:extra-constructor-name make-bc-token)
  access-token : string?
  expires-at : exact-integer?
  refresh-token : (or/c #f string?)
Represents an OAuth access token.

  • access-token: The bearer token for API requests

  • expires-at: Unix timestamp when the token expires

  • refresh-token: Refresh token (only present from interactive auth)

10.5 Result Wrapper🔗

Entity list operations (like customers-list) return a bc-result struct rather than a plain list. This wrapper provides:

  • Safe REPL printing — Large datasets display a summary instead of hanging

  • O(1) access — Count and random element access without traversing the list

  • Iteration support — Use directly in for loops

  • Metadata — Tracks which entity, query, cache status, and fetch time

struct

(struct bc-result (records entity query cached? fetched-at))

  records : (vectorof hash?)
  entity : bc-entity?
  query : (or/c #f query?)
  cached? : boolean?
  fetched-at : exact-integer?
Wraps the results of an entity list operation.

  • records The fetched records as a vector of hash tables

  • entity The bc-entity that was queried

  • query The OData query used, or #f if none

  • cached? #t if all records came from cache

  • fetched-at Unix timestamp when the data was fetched

REPL printing: Displays a compact summary instead of all records:

> (customers-list)

#<bc-result: customers (42 records, ~125 KB)>

Iteration: Implements prop:sequence, so you can iterate directly:
(for ([cust (customers-list)])
  (displayln (hash-ref cust 'displayName)))

Contract for bc-result values. Useful for defining your own functions that accept or return results.

procedure

(make-bc-result records    
  entity    
  [#:query query    
  #:cached? cached?    
  #:fetched-at fetched-at])  bc-result?
  records : (or/c (listof hash?) (vectorof hash?))
  entity : bc-entity?
  query : (or/c #f query?) = #f
  cached? : boolean? = #f
  fetched-at : exact-integer? = (current-seconds)
Creates a bc-result. Primarily used internally by entity list functions, but available if you need to construct results manually (e.g., for testing).

10.5.1 Result Accessors🔗

procedure

(bc-result-count result)  exact-nonnegative-integer?

  result : bc-result?
Returns the number of records. O(1) time complexity.

(bc-result-count (customers-list))  ;; -> 42

procedure

(bc-result-ref result index)  hash?

  result : bc-result?
  index : exact-nonnegative-integer?
Returns the record at index. O(1) time complexity.

(bc-result-ref (customers-list) 0)  ;; -> first customer hash

procedure

(bc-result-first result [n])  (vectorof hash?)

  result : bc-result?
  n : exact-positive-integer? = 5
Returns the first n records as a vector. O(n) time complexity. Returns fewer than n if the result has fewer records.

procedure

(bc-result->list result)  (listof hash?)

  result : bc-result?
Converts all records to a list. O(n) time complexity.

Use this when you need list operations like filter, map, or length:

(define all-customers (bc-result->list (customers-list)))
(filter (lambda (c) (hash-ref c 'blocked #f)) all-customers)

For simple iteration, prefer for directly on the result (more efficient).

procedure

(bc-result-size result)  exact-nonnegative-integer?

  result : bc-result?
Returns the estimated size of all records in bytes. O(1) after first call (cached).

Useful for understanding memory usage of large datasets.

procedure

(bc-result-empty? result)  boolean?

  result : bc-result?
Returns #t if the result contains no records. O(1) time complexity.

syntax

(in-bc-result result-expr)

Sequence syntax for efficient iteration over a bc-result in for forms.

While bc-result implements prop:sequence and can be used directly in for clauses, using in-bc-result is more efficient because it expands at compile time directly to in-vector, avoiding runtime sequence dispatch.

(define result (customers-list))
 
;; Both work, but in-bc-result is faster:
(for ([c result]) ...)              ; runtime dispatch via prop:sequence
(for ([c (in-bc-result result)]) ...) ; compile-time expansion

10.6 Exceptions🔗

bcnav defines a hierarchy of exception types for different error conditions.

struct

(struct exn:fail:bcnav exn:fail ()
    #:extra-constructor-name make-exn:fail:bcnav)
Base exception type for all bcnav errors.

struct

(struct exn:fail:bcnav:auth exn:fail:bcnav ()
    #:extra-constructor-name make-exn:fail:bcnav:auth)
Raised for authentication failures (invalid credentials, token issues).

struct

(struct exn:fail:bcnav:http exn:fail:bcnav (method
    endpoint
    status-code
    body)
    #:extra-constructor-name make-exn:fail:bcnav:http)
  method : symbol?
  endpoint : string?
  status-code : exact-integer?
  body : string?
Raised for HTTP API errors (4xx and 5xx responses).

The body field contains the response body, typically a JSON error message from Business Central.

struct

(struct exn:fail:bcnav:rate-limit exn:fail:bcnav (retry-after)
    #:extra-constructor-name make-exn:fail:bcnav:rate-limit)
  retry-after : string?
Raised when rate limiting is encountered and retries are exhausted. The retry-after field indicates how long to wait.

10.7 Logging🔗

procedure

(start-bc-listener [level])  void?

  level : (or/c 'debug 'info 'warning 'error) = 'debug
Starts a thread that prints bcnav log messages to the current error port. Messages at or above level are displayed.

procedure

(stop-bc-listener)  void?

Stops the log listener thread.

procedure

(log-bc-debug format-string arg ...)  void?

  format-string : string?
  arg : any/c
Logs a debug-level message.

procedure

(log-bc-info format-string arg ...)  void?

  format-string : string?
  arg : any/c
Logs an info-level message.

procedure

(log-bc-warning format-string arg ...)  void?

  format-string : string?
  arg : any/c
Logs a warning-level message.

procedure

(log-bc-error format-string arg ...)  void?

  format-string : string?
  arg : any/c
Logs an error-level message.

10.8 Admin Center API🔗

The Admin Center API provides tenant-level management operations, including discovering available Business Central environments.

10.8.1 Configuration🔗

parameter

(current-bc-admin-api-version)  string?

(current-bc-admin-api-version version)  void?
  version : string?
 = "v2.21"
The Admin Center API version to use. This is separate from the BC v2.0 entity API version. The default value should work for most cases; only change this if Microsoft updates the Admin Center API and you need a newer version.

10.8.2 Environment Discovery🔗

Before connecting to a specific BC environment, you can discover which environments are available for your tenant.

struct

(struct bc-environment (name
    type
    status
    aad-tenant-id
    application-family
    country-code
    webClientLoginUrl
    properties))
  name : string?
  type : string?
  status : string?
  aad-tenant-id : string?
  application-family : string?
  country-code : string?
  webClientLoginUrl : (or/c #f string?)
  properties : hash?
Represents a Business Central environment.

  • name The environment name (e.g., "Production", "Sandbox")

  • type Either "Production" or "Sandbox"

  • status Current status (e.g., "Active", "Preparing")

  • aad-tenant-id The Azure AD tenant ID

  • application-family Usually "BusinessCentral"

  • country-code Two-letter country code (e.g., "US", "DK")

  • webClientLoginUrl URL to access the web client, or #f

  • properties The full API response hash with additional properties

Contract for bc-environment values.

Returns a list of all Business Central environments accessible to the authenticated user.

This function uses the Admin Center API, which requires only tenant-level authentication. You must call bc-authenticate! or bc-authenticate/cached! before using this function, but you do not need to set current-bc-environment or current-bc-company first.

Typical workflow:
;; Set credentials
(current-bc-tenant "contoso.onmicrosoft.com")
(current-bc-client-id "your-client-id")
(current-bc-client-secret "your-secret")
 
;; Authenticate
(bc-authenticate!)
 
;; Discover environments
(for ([env (environments-list)])
  (printf "~a (~a): ~a~n"
          (bc-environment-name env)
          (bc-environment-type env)
          (bc-environment-status env)))
 
;; Now set the environment you want to use
(current-bc-environment "Production")

Example output:

Production (Production): Active

Sandbox (Sandbox): Active

DevTest (Sandbox): Active