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))
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 |
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
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.