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
A tenant GUID: "12345678-1234-1234-1234-123456789012"
A domain name: "contoso.onmicrosoft.com"
parameter
(current-bc-environment env) → void? env : string?
= "Production"
parameter
(current-bc-company) → (or/c #f string?)
(current-bc-company company) → void? company : (or/c #f string?)
= #f
parameter
(current-bc-client-id) → (or/c #f string?)
(current-bc-client-id client-id) → void? client-id : (or/c #f string?)
= #f
parameter
(current-bc-client-secret) → (or/c #f string?)
(current-bc-client-secret secret) → void? secret : (or/c #f string?)
= #f
parameter
(current-bc-redirect-uri uri) → void? uri : string?
= "http://localhost:8080/oauth/authorization"
10.2 Behavior Parameters
parameter
(current-bc-auto-paginate paginate?) → void? paginate? : boolean?
= #t
parameter
(current-bc-log-requests log?) → void? log? : boolean?
= #f
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
10.3 Response Caching
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
When set, the cache is always consulted before making GET requests—
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
'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
(bc-cache-put! entity data) —
Cache the list endpoint response (bc-cache-put! entity data query) —
Cache a filtered list response (bc-cache-put! entity id data) —
Cache a single record by ID
;; 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?)
(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?)
(bc-cache-remove! entity) —
Remove list endpoint cache (bc-cache-remove! entity query) —
Remove filtered list cache (bc-cache-remove! entity id) —
Remove single record cache
10.3.3 Cache Utilities
procedure
(cache-list) → (listof hash?)
'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?
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)
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")
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
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
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
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
procedure
(token-valid?) → boolean?
procedure
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?)
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?
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
> (customers-list) |
#<bc-result: customers (42 records, ~125 KB)> |
(for ([cust (customers-list)]) (displayln (hash-ref cust 'displayName)))
value
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)
10.5.1 Result Accessors
procedure
(bc-result-count result) → exact-nonnegative-integer?
result : bc-result?
(bc-result-count (customers-list)) ;; -> 42
procedure
(bc-result-ref result index) → hash?
result : bc-result? index : exact-nonnegative-integer?
(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
procedure
(bc-result->list result) → (listof hash?)
result : bc-result?
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?
Useful for understanding memory usage of large datasets.
procedure
(bc-result-empty? result) → boolean?
result : bc-result?
syntax
(in-bc-result result-expr)
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:auth exn:fail:bcnav () #:extra-constructor-name make-exn:fail:bcnav:auth)
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?
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?
10.7 Logging
procedure
(start-bc-listener [level]) → void?
level : (or/c 'debug 'info 'warning 'error) = 'debug
procedure
procedure
(log-bc-debug format-string arg ...) → void?
format-string : string? arg : any/c
procedure
(log-bc-info format-string arg ...) → void?
format-string : string? arg : any/c
procedure
(log-bc-warning format-string arg ...) → void?
format-string : string? arg : any/c
procedure
(log-bc-error format-string arg ...) → void?
format-string : string? arg : any/c
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 version) → void? version : string?
= "v2.21"
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?
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
value
procedure
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.
;; 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")
Production (Production): Active |
Sandbox (Sandbox): Active |
DevTest (Sandbox): Active |