| name | bc-api-page-generator |
| description | Automatically generate RESTful API page objects for Business Central following OData and API v2.0 best practices. Creates API pages with proper field mappings, navigation properties, custom actions, error handling, and performance optimization. Use when user requests to create, generate, or build an API endpoint, expose data via API, create REST service, or mentions OData/web service integration. Trigger phrases include "create API page", "generate API endpoint for [entity]", "build REST API", "expose [table] via API", or when implementing web service integrations. |
Business Central API Page Generator
Overview
Generates AL API page objects following Microsoft's API v2.0 standards and OData best practices. Creates production-ready RESTful APIs with proper field mappings, navigation properties, custom actions, authentication considerations, and performance optimization.
Quick Start
Basic usage:
// Create an API v2.0 page for exposing data
// Pattern: "[EntityName] API" for page name
// Pattern: lowercase camelCase for entity/field names
Example requests:
- "Create an API page for Sales Orders"
- "Generate an API endpoint to expose customers"
- "Build a REST API for items"
- "Create API pages for sales header and lines"
Prerequisites
- Source table exists with proper primary key
- Understanding of which fields should be exposed
- Available page ID (use Object ID Ninja or manual allocation)
- Knowledge of authentication/permission requirements
- Consider performance and filtering needs
- Clear description of API purpose for AboutText property
Core API Page Structure
1. API Page Properties (v2.0 Standard)
page [ID] "[EntityName] API"
{
APIVersion = 'v2.0';
APIPublisher = 'mycompany'; // Your company/publisher name
APIGroup = '[group]'; // Logical grouping (sales, inventory, finance)
EntityCaption = '[Entity Name]';
EntitySetCaption = '[Entity Names]';
EntityName = '[entityName]'; // Singular, camelCase
EntitySetName = '[entityNames]'; // Plural, camelCase
PageType = API;
SourceTable = "[Source Table Name]";
DelayedInsert = true;
ODataKeyFields = SystemId; // Use SystemId for stability
AboutText = 'API endpoint for [entity description]. Supports GET, POST, PATCH, DELETE operations.';
Extensible = false; // Prevent extensions that might break API contract
}
Key property guidelines:
- APIVersion: Use 'v2.0' for modern APIs
- APIPublisher: Your company identifier (lowercase, no spaces)
- APIGroup: Logical grouping (sales, purchasing, inventory, finance)
- EntityName/EntitySetName: camelCase, no spaces, descriptive
- ODataKeyFields: Prefer SystemId over business keys for stability
- DelayedInsert: Set to true for better API behavior
- AboutText: Clear description of API purpose and supported operations (required)
Endpoint URL will be:
/api/v2.0/companies({companyId})/[entityNames]
Example: /api/v2.0/companies({companyId})/salesOrders
2. Layout Pattern for API Pages
API pages use special field patterns optimized for JSON serialization:
layout
{
area(Content)
{
repeater(Group)
{
// 1. System ID (always first, read-only)
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
// 2. Business key fields (often read-only after creation)
field(number; Rec."No.")
{
Caption = 'Number';
Editable = false; // If auto-generated
}
// 3. Required business fields
field(customerNumber; Rec."Sell-to Customer No.")
{
Caption = 'Customer Number';
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
// 4. Optional business fields
field(customerName; Rec."Sell-to Customer Name")
{
Caption = 'Customer Name';
Editable = false; // Calculated/FlowField
}
// 5. Calculated/summary fields
field(totalAmount; Rec."Amount Including VAT")
{
Caption = 'Total Amount';
Editable = false;
}
// 6. Status fields
field(status; Rec.Status)
{
Caption = 'Status';
Editable = false; // Often controlled by business logic
}
// 7. Timestamp fields (always at end)
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
// 8. Navigation properties (part/lines)
part(salesOrderLines; "[EntityName] Lines API")
{
Caption = 'Lines';
EntityName = '[entityName]Line';
EntitySetName = '[entityName]Lines';
SubPageLink = "Document Type" = field("Document Type"),
"Document No." = field("No.");
}
}
}
}
Field naming conventions:
- Use camelCase for field identifiers
- Use clear, descriptive captions
- Mark calculated/FlowFields as Editable = false
- Mark system fields as Editable = false
- Group related fields logically
3. Actions Pattern for API Operations
actions
{
area(Processing)
{
// Bound action: operates on specific record
// URL: POST /entityName({id})/Microsoft.NAV.actionName
action(Post)
{
ApplicationArea = All;
Caption = 'Post';
trigger OnAction()
var
[PostingCodeunit]: Codeunit "[Posting Codeunit]";
begin
[PostingCodeunit].Run(Rec);
// Optional: return result via SetActionResponse
end;
}
action(Cancel)
{
ApplicationArea = All;
Caption = 'Cancel';
trigger OnAction()
begin
// Business logic
Rec.Status := Rec.Status::Cancelled;
Rec.Modify(true);
end;
}
// Action with parameters (accepts POST body)
action(ApplyDiscount)
{
ApplicationArea = All;
Caption = 'Apply Discount';
trigger OnAction()
var
DiscountPct: Decimal;
begin
// Get parameter from request body
DiscountPct := GetActionParameter('discountPercent');
// Apply business logic
ApplyDiscountToDocument(Rec, DiscountPct);
// Return result
SetActionResponse(Rec."Amount Including VAT");
end;
}
}
}
Action guidelines:
- Actions become POST operations:
/entityName({id})/Microsoft.NAV.actionName
- Use for operations that change state or perform complex logic
- Keep action names clear and verb-based
- Consider returning meaningful results via SetActionResponse
4. Trigger Pattern for Validation
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Validate required fields
if Rec."[RequiredField]" = '' then
Error('[FieldName] is required');
// Call validation logic
ValidateRecord(Rec);
exit(true);
end;
trigger OnModifyRecord(): Boolean
begin
// Prevent modification if document is posted/locked
if Rec.Status = Rec.Status::Released then
Error('Cannot modify released document');
// Additional validation
ValidateRecord(Rec);
exit(true);
end;
trigger OnDeleteRecord(): Boolean
begin
// Check if deletion is allowed
if HasRelatedRecords(Rec) then
Error('Cannot delete record with related data');
exit(true);
end;
local procedure ValidateRecord(var Record: Record "[SourceTable]")
begin
// Centralized validation logic
// Check business rules, credit limits, inventory, etc.
end;
Validation principles:
- Validate early in Insert/Modify triggers
- Return clear error messages
- Use centralized validation procedures
- Check referential integrity
- Enforce business rules
Complete API Page Examples
Example 1: Simple API Page (Single Table)
page 50100 "Items API"
{
APIVersion = 'v2.0';
APIPublisher = 'mycompany';
APIGroup = 'inventory';
EntityCaption = 'Item';
EntitySetCaption = 'Items';
EntityName = 'item';
EntitySetName = 'items';
PageType = API;
SourceTable = Item;
DelayedInsert = true;
ODataKeyFields = SystemId;
AboutText = 'API endpoint for managing inventory items. Supports GET, POST, PATCH, DELETE operations.';
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
Editable = false;
}
field(description; Rec.Description)
{
Caption = 'Description';
}
field(type; Rec.Type)
{
Caption = 'Type';
}
field(baseUnitOfMeasure; Rec."Base Unit of Measure")
{
Caption = 'Base Unit Of Measure';
}
field(unitPrice; Rec."Unit Price")
{
Caption = 'Unit Price';
}
field(unitCost; Rec."Unit Cost")
{
Caption = 'Unit Cost';
}
field(inventory; Rec.Inventory)
{
Caption = 'Inventory';
Editable = false;
}
field(blocked; Rec.Blocked)
{
Caption = 'Blocked';
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
}
}
}
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
if Rec.Description = '' then
Error('Description is required');
exit(true);
end;
}
Example 2: Header-Lines API Pages
Header Page:
page 50110 "Sales Orders API"
{
APIVersion = 'v2.0';
APIPublisher = 'mycompany';
APIGroup = 'sales';
EntityCaption = 'Sales Order';
EntitySetCaption = 'Sales Orders';
EntityName = 'salesOrder';
EntitySetName = 'salesOrders';
PageType = API;
SourceTable = "Sales Header";
DelayedInsert = true;
ODataKeyFields = SystemId;
AboutText = 'API endpoint for managing sales orders. Supports GET, POST, PATCH, DELETE operations with order lines navigation.';
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
Editable = false;
}
field(customerNumber; Rec."Sell-to Customer No.")
{
Caption = 'Customer Number';
}
field(customerName; Rec."Sell-to Customer Name")
{
Caption = 'Customer Name';
Editable = false;
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
field(shipmentDate; Rec."Shipment Date")
{
Caption = 'Shipment Date';
}
field(totalAmount; Rec."Amount Including VAT")
{
Caption = 'Total Amount';
Editable = false;
}
field(status; Rec.Status)
{
Caption = 'Status';
Editable = false;
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
// Navigation to lines
part(salesOrderLines; "Sales Order Lines API")
{
Caption = 'Lines';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
SubPageLink = "Document Type" = field("Document Type"),
"Document No." = field("No.");
}
}
}
}
actions
{
area(Processing)
{
action(Post)
{
ApplicationArea = All;
Caption = 'Post';
trigger OnAction()
var
SalesPost: Codeunit "Sales-Post";
begin
SalesPost.Run(Rec);
end;
}
}
}
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
Rec."Document Type" := Rec."Document Type"::Order;
if Rec."Sell-to Customer No." = '' then
Error('Customer Number is required');
exit(true);
end;
trigger OnModifyRecord(): Boolean
begin
if Rec.Status = Rec.Status::Released then
Error('Cannot modify released sales order');
exit(true);
end;
}
Lines Page:
page 50111 "Sales Order Lines API"
{
APIVersion = 'v2.0';
APIPublisher = 'mycompany';
APIGroup = 'sales';
EntityCaption = 'Sales Order Line';
EntitySetCaption = 'Sales Order Lines';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
PageType = API;
SourceTable = "Sales Line";
DelayedInsert = true;
ODataKeyFields = SystemId;
AboutText = 'API endpoint for managing sales order line items. Supports GET, POST, PATCH, DELETE operations.';
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(documentId; Rec."Document SystemId")
{
Caption = 'Document Id';
}
field(lineNumber; Rec."Line No.")
{
Caption = 'Line Number';
}
field(type; Rec.Type)
{
Caption = 'Type';
}
field(itemNumber; Rec."No.")
{
Caption = 'Item Number';
}
field(description; Rec.Description)
{
Caption = 'Description';
}
field(quantity; Rec.Quantity)
{
Caption = 'Quantity';
}
field(unitOfMeasure; Rec."Unit of Measure Code")
{
Caption = 'Unit Of Measure';
}
field(unitPrice; Rec."Unit Price")
{
Caption = 'Unit Price';
}
field(lineAmount; Rec."Line Amount")
{
Caption = 'Line Amount';
Editable = false;
}
field(lineDiscount; Rec."Line Discount %")
{
Caption = 'Line Discount %';
}
}
}
}
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
if Rec."Document SystemId" = EmptyGuid() then
Error('Document Id is required');
if Rec."No." = '' then
Error('Item Number is required');
exit(true);
end;
}
API Design Workflow
Step 1: Define API Purpose
Ask these questions:
- Who will consume this API? (external partners, mobile apps, Power Platform)
- What operations are needed? (Read, Create, Update, Delete, Custom actions)
- What data should be exposed?
- Are there performance requirements?
- What authentication method?
- What versioning strategy?
Step 2: Plan Resource Model
- Identify entities (customers, salesOrders, items)
- Define relationships (customer → salesOrders → salesOrderLines)
- Plan navigation properties
- Consider filtering needs
- Design custom actions if needed
Step 3: Allocate Object IDs
Use Object ID Ninja or manual allocation:
- API pages typically in 50100-50199 range
- Keep header and lines pages close together (e.g., 50110, 50111)
- Document IDs in project tracker
Step 4: Implement API Page(s)
- Start with header page
- Add lines page if header-lines pattern
- Implement fields following patterns above
- Add validation triggers
- Add custom actions if needed
Step 5: Test API
Create basic tests:
### Get all records
GET {{baseUrl}}/api/v2.0/companies({{companyId}})/[entityNames]
Authorization: Bearer {{token}}
### Get specific record
GET {{baseUrl}}/api/v2.0/companies({{companyId}})/[entityNames]({{id}})
Authorization: Bearer {{token}}
### Create record
POST {{baseUrl}}/api/v2.0/companies({{companyId}})/[entityNames]
Authorization: Bearer {{token}}
Content-Type: application/json
{
"fieldName": "value"
}
### Update record
PATCH {{baseUrl}}/api/v2.0/companies({{companyId}})/[entityNames]({{id}})
Authorization: Bearer {{token}}
Content-Type: application/json
If-Match: {{etag}}
{
"fieldName": "newValue"
}
Field Naming Best Practices
AL Field ID → JSON Field Name Mapping
| AL Field | JSON Field | Pattern |
|---|
"No." | number | Use descriptive names |
"Sell-to Customer No." | customerNumber | camelCase compound words |
"Order Date" | orderDate | Remove spaces |
"Amount Including VAT" | totalAmount | Simplify when clear |
Status | status | Lowercase single words |
SystemId | id | Standard system fields |
SystemModifiedAt | lastModifiedDateTime | Descriptive timestamps |
Rules:
- Use camelCase consistently
- Remove spaces and special characters
- Be descriptive but concise
- Follow REST/JSON conventions
- Use standard names for common concepts (id, number, description, status)
Performance Considerations
1. Support Filtering
Ensure proper keys exist on source table:
table "Your Table"
{
keys
{
key(PK; "No.") { Clustered = true; }
key(Customer; "Customer No.", "Order Date") { }
key(Status; Status, "Order Date") { }
}
}
2. Use SetLoadFields
trigger OnAfterGetRecord()
begin
Rec.SetLoadFields("No.", "Description", "Unit Price");
end;
3. Limit FlowFields in API
- Avoid heavy FlowFields that require calculation
- Mark complex FlowFields as
Visible = false unless critical
- Consider dedicated summary endpoints for aggregations
Security & Permissions
Create Permission Sets
permissionset 50100 "API Access - [Group]"
{
Assignable = true;
Caption = 'API Access - [Group]';
Permissions =
page "[EntityName] API" = X,
tabledata "[SourceTable]" = RIMD;
}
permissionset 50101 "API Read - [Group]"
{
Assignable = true;
Caption = 'API Read Only - [Group]';
Permissions =
page "[EntityName] API" = X,
tabledata "[SourceTable]" = R;
}
Common Patterns
Pattern 1: Read-Only API
page 50120 "Customers API"
{
// ... standard properties ...
Editable = false; // Make entire page read-only
InsertAllowed = false;
ModifyAllowed = false;
DeleteAllowed = false;
layout
{
area(Content)
{
repeater(Group)
{
// All fields automatically non-editable
}
}
}
}
Pattern 2: Calculated/Virtual Fields
field(creditLimitExceeded; IsCreditLimitExceeded())
{
Caption = 'Credit Limit Exceeded';
Editable = false;
}
local procedure IsCreditLimitExceeded(): Boolean
begin
exit(Rec."Balance (LCY)" > Rec."Credit Limit (LCY)");
end;
Pattern 3: Enum/Option Fields
field(status; Rec.Status)
{
Caption = 'Status';
}
// API returns: "status": "open" or "status": "released"
// API accepts: { "status": "open" } or { "status": "released" }
When to Create API Pages
Create API pages when:
- ✅ External systems need to integrate with BC
- ✅ Mobile apps need data access
- ✅ Power Platform needs to consume BC data
- ✅ Automation/batch processes need programmatic access
- ✅ Third-party applications need REST endpoints
Don't create API pages when:
- ❌ Only internal BC users need access (use regular pages)
- ❌ One-time data migration (use XMLports or DataExchange)
- ❌ Complex UI interactions required (use client pages)
Checklist
Before completing API page generation:
References
Tips & Tricks
- Use SystemId for ODataKeyFields - More stable than business keys
- DelayedInsert = true - Better API POST behavior
- Test with Postman - Easier than browser for POST/PATCH/DELETE
- Version early - Plan for v2.0, v3.0 from the start
- Document endpoints - Create OpenAPI/Swagger documentation
- Monitor performance - Use telemetry to track API usage
- Handle etags properly - Use If-Match header for concurrency
Skill Version: 1.0
Last Updated: 2026-01-19
Maintained by: BC Development Team