On this page:
7.1 Defining standard API entities
7.2 Understanding BC API URLs
7.3 What macros generate
7.4 Defining a custom API
7.5 Defining entities
7.6 Complete example
7.7 Generated functions
7.8 Inspecting custom entities
7.9 Finding available APIs
7.10 Example:   Working with a real publisher API
7.11 Best practices
7.11.1 One module per API
7.11.2 Document your entities
7.11.3 Version your APIs
7.12 Next steps
9.3

7 Custom APIs🔗

Business Central’s standard API covers many common entities, but your BC installation might have additional APIs:

  • Publisher APIs: Added by Microsoft or third-party extensions (ISV apps)

  • Custom APIs: Built by your organization using AL code

These APIs follow the same OData patterns as the standard API but live at different URL paths. bcnav provides macros for defining access to them.

7.1 Defining standard API entities🔗

Before diving into custom APIs, note that define-entity can also be used to access standard BC API entities that don’t have predefined wrappers in bcnav. For example, if you need to work with taxGroups or unitsOfMeasure:

;; #lang racket/base
 
(require bcnav/api/custom)
 
;; Access standard BC entities not pre-defined in bcnav
(define-entity tax-group "taxGroups")
(define-entity unit-of-measure "unitsOfMeasure")
(define-entity time-reg "timeRegistrationEntries")
 
(provide (entity-out tax-group)
         (entity-out unit-of-measure)
         (entity-out time-reg))

Each define-entity creates:
  • A bc-entity struct value (tax-group) for use with inspect, peek, and sample

  • CRUD functions: tax-group-list, tax-group-get, tax-group-create, tax-group-update, tax-group-delete

The entity-out provide transformer exports all these bindings from your module (similar to how struct-out works for structs).

Important: Entity names are not validated at definition time. If you misspell an entity name or use one that doesn’t exist, you won’t get an error until you actually make an API request.

7.2 Understanding BC API URLs🔗

The standard BC API lives at a URL like:

https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/v2.0/companies({company})/customers

Custom and publisher APIs have a different structure:

https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/{publisher}/{group}/{version}/companies({company})/widgets

The key additions are:
  • publisher: Who created the API (e.g., microsoft, contoso)

  • group: A grouping name for related APIs (e.g., inventory, sales)

  • version: The API version (e.g., v1.0, v2.0)

7.3 What macros generate🔗

bcnav uses macros to generate code at compile time. When you write:

(define-custom-api my-api "contoso" "inventory" "v1.0")
(define-entity widget my-api "widgets")
(provide (entity-out widget))

The macros create these bindings, and entity-out exports them:

  • widget A bc-entity struct value containing the entity name and API info. Use with inspect to see the schema, peek to preview data as a table, or sample to get raw data.

  • widget-list List all widgets with optional #:query

  • widget-get Get one widget by ID

  • widget-create Create a new widget

  • widget-update Update a widget (supports #:etag)

  • widget-delete Delete a widget (supports #:etag)

7.4 Defining a custom API🔗

Start by creating a module for your custom API:

;; #lang racket/base
 
(require bcnav/api/custom)
 
;; Define the API namespace
(define-custom-api inventory-api "contoso" "inventory" "v1.0")

The define-custom-api macro creates a custom-api-info struct that holds the publisher, group, and version. This struct is used by define-entity to construct the correct API URLs.

7.5 Defining entities🔗

Use define-entity to create the entity struct and CRUD functions, then entity-out to export them:

(define-entity widget inventory-api "widgets")
(define-entity gadget inventory-api "gadgets")
 
(provide (entity-out widget)
         (entity-out gadget))

define-entity has two forms:

Form

API Type

(define-entity name "entityName")

Standard BC v2.0 API

(define-entity name api-info "entityName")

Custom/publisher API

For read-only entities (no create, update, or delete), use define-entity/read-only. When used with entity-out, only the read bindings are exported:

(define-entity/read-only readonly-thing inventory-api "readOnlyThings")
(provide (entity-out readonly-thing))
;; Exports: readonly-thing, readonly-thing-list, readonly-thing-get

7.6 Complete example🔗

Here’s a complete module for a fictional custom API:

;; #lang racket/base
 
;; contoso-inventory.rkt
;; Access to Contoso’s custom inventory API
 
(require bcnav/api/custom)
 
;; Define the API
(define-custom-api contoso-api "contoso" "inventory" "v1.0")
 
;; Define entities
(define-entity warehouse contoso-api "warehouses")
(define-entity bin contoso-api "bins")
(define-entity stock-count contoso-api "stockCounts")
 
;; Export all entity bindings
(provide (entity-out warehouse)
         (entity-out bin)
         (entity-out stock-count))

Save this as a file (e.g., "contoso-inventory.rkt") and use it:

;; #lang racket
 
(require bcnav
         "contoso-inventory.rkt")
 
;; Configure and authenticate (as usual)
(current-bc-tenant (getenv "BC_TENANT_ID"))
;; ... etc ...
(bc-authenticate!)
 
;; Inspect the schema
(inspect warehouse)
 
;; Preview data as a table
(peek warehouse)
 
;; Or get raw data
(sample warehouse)
 
;; Use the custom API
(warehouse-list)
(warehouse-get "some-warehouse-id")
(warehouse-create (hasheq 'code "WH-001"
                          'name "Main Warehouse"))

7.7 Generated functions🔗

define-entity creates five functions following the standard bcnav pattern:

Function

Description

widget-list

List all records, with optional #:query

widget-get

Get one record by ID

widget-create

Create a new record

widget-update

Update a record (supports #:etag)

widget-delete

Delete a record (supports #:etag)

They work exactly like the standard entity functions:

;; List with query
(widget-list #:query (make-query
                       #:filter (= 'status "Active")
                       #:top 50))
 
;; Get by ID
(widget-get "abc123-...")
 
;; Create
(widget-create (hasheq 'name "New Widget"
                       'category "Mechanical"))
 
;; Update
(widget-update "abc123-..." (hasheq 'status "Inactive"))
 
;; Delete
(widget-delete "abc123-..." #:etag "W/\"xyz789...\"")

7.8 Inspecting custom entities🔗

Use the same inspection functions with custom entities as with standard ones:

;; Visual schema display
(inspect widget)
 
;; Preview data as a table
(peek widget)
(peek widget 10)
 
;; Or get raw data for processing
(sample widget)
(sample widget 10)
 
;; Generate documentation
(displayln (inspect->comment widget))
(displayln (inspect->scribble widget))

7.9 Finding available APIs🔗

BC doesn’t provide a direct way to list all installed APIs, but you can:

  • Check your BC extensions to see what APIs they expose

  • Look at AL source code for custom API pages

  • Ask your BC administrator or developer

  • Try accessing the metadata endpoint for suspected APIs

If you try to access an API that doesn’t exist, you’ll get a 404 error.

7.10 Example: Working with a real publisher API🔗

Here’s how you might set up access to a real-world publisher API:

;; #lang racket/base
 
;; erik-hougaard-api.rkt
;; Access to Erik Hougaard’s BC YouTube tutorial APIs
 
(require bcnav/api/custom)
 
(define-custom-api hougaard-api "hougaard" "youtube" "v2.0")
 
(define-entity car hougaard-api "cars")
(define-entity car-brand hougaard-api "carBrands")
 
(provide (entity-out car)
         (entity-out car-brand))

Then in your script:

;; #lang racket
 
(require bcnav
         "erik-hougaard-api.rkt")
 
;; ... authentication ...
 
;; Explore the schema
(inspect car)
 
;; Preview some data
(peek car)
 
;; List all cars
(define all-cars (car-list))
 
;; Find cars by brand
(define toyota-cars
  (car-list #:query (make-query
                      #:filter (= 'brandCode "TOYOTA"))))
 
;; Add a new car
(car-create (hasheq 'licensePlate "ABC-123"
                    'brandCode "TOYOTA"
                    'model "Camry"))

7.11 Best practices🔗

7.11.1 One module per API🔗

Keep each custom API in its own module file. This makes it easy to:
  • Share the module across multiple scripts

  • Update entity definitions in one place

  • Document what’s available

7.11.2 Document your entities🔗

Add comments explaining what each entity is for:

;; Widget: Custom inventory tracking items
;; Fields: id, name, category, status, quantity, lastCountDate
;; Expands: warehouse, stockMovements
(define-entity widget inventory-api "widgets")
(provide (entity-out widget))

Or better yet, use inspect->comment to generate accurate documentation:

;; Run this once, then paste the output into your module
(displayln (inspect->comment widget))

7.11.3 Version your APIs🔗

Custom APIs can change. Include the version in your module name or comments:

;; contoso-inventory-v1.rkt
;; Compatible with Contoso Inventory Extension v1.0 - v1.3

7.12 Next steps🔗

Continue to Logging & Debugging to learn how to troubleshoot API calls and see what’s happening behind the scenes.