| name | skill-api |
| description | AL API development patterns for Business Central. Use when creating OData/REST API pages, HttpClient integrations, webhook implementations, or any external system integration via API. |
Skill: AL API Development
Purpose
Design and implement RESTful API pages for Business Central: API page v2.0 patterns, OData conventions, versioning, bound/unbound actions, webhooks, header-lines navigation, and performance-optimized endpoints.
When to Load
This skill should be loaded when:
- A new API page (PageType = API) needs to be designed or implemented
- Existing BC data must be exposed to external consumers (Power Platform, mobile apps, 3rd-party)
- Custom bound or unbound actions are needed on API endpoints
- An API versioning strategy or deprecation plan is required
- Webhook subscriptions need to be configured for change notifications
- API performance or filtering needs optimization
Core Patterns
Pattern 1: API Page v2.0 (Standard CRUD Endpoint)
page 50100 "Contoso Sales Orders API"
{
APIVersion = 'v2.0';
APIPublisher = 'contoso';
APIGroup = 'sales';
EntityCaption = 'Sales Order';
EntitySetCaption = 'Sales Orders';
EntityName = 'salesOrder'; // singular — used in URL for single entity
EntitySetName = 'salesOrders'; // plural — used in URL for collection
PageType = API;
SourceTable = "Sales Header";
SourceTableView = where("Document Type" = const(Order));
DelayedInsert = true; // required — defers insert until all fields set
ODataKeyFields = SystemId; // required — use SystemId for stable GUIDs
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
Editable = false;
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
field(customerNumber; Rec."Sell-to Customer No.")
{
Caption = 'Customer Number';
}
field(customerName; Rec."Sell-to Customer Name")
{
Caption = 'Customer Name';
Editable = false;
}
field(totalAmountIncludingVAT; Rec."Amount Including VAT")
{
Caption = 'Total Amount Including VAT';
Editable = false;
}
field(status; Rec.Status)
{
Caption = 'Status';
Editable = false;
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified Date Time';
Editable = false;
}
}
}
}
}
Resulting endpoint:
GET /api/contoso/sales/v2.0/companies({companyId})/salesOrders
GET /api/contoso/sales/v2.0/companies({companyId})/salesOrders({id})
POST /api/contoso/sales/v2.0/companies({companyId})/salesOrders
PATCH /api/contoso/sales/v2.0/companies({companyId})/salesOrders({id})
DELETE /api/contoso/sales/v2.0/companies({companyId})/salesOrders({id})
Property rules:
ODataKeyFields = SystemId — always use SystemId for stable, immutable keys
DelayedInsert = true — mandatory on API pages (lets BC set defaults before committing)
SourceTableView — pre-filter the source if the table serves multiple document types
- Field names use camelCase (OData convention):
customerNumber, not Customer_Number
Editable = false on computed/system fields to prevent consumer confusion
Pattern 2: Header-Lines with Navigation Property
Expose parent-child relationships via part subpages:
// Add inside the header API page's repeater:
part(salesOrderLines; "Contoso Sales Order Lines API")
{
Caption = 'Lines';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
SubPageLink = "Document Type" = field("Document Type"),
"Document No." = field("No.");
}
// Lines API page (subpage)
page 50101 "Contoso Sales Order Lines API"
{
APIVersion = 'v2.0';
APIPublisher = 'contoso';
APIGroup = 'sales';
EntityCaption = 'Sales Order Line';
EntitySetCaption = 'Sales Order Lines';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
PageType = API;
SourceTable = "Sales Line";
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId) { Editable = false; }
field(lineNumber; Rec."Line No.") { Editable = false; }
field(lineType; Rec.Type) { Caption = 'Type'; }
field(itemNumber; Rec."No.") { Caption = 'Item Number'; }
field(description; Rec.Description) { Caption = 'Description'; }
field(quantity; Rec.Quantity) { Caption = 'Quantity'; }
field(unitPrice; Rec."Unit Price") { Caption = 'Unit Price'; }
field(lineAmount; Rec."Line Amount") { Caption = 'Line Amount'; Editable = false; }
}
}
}
}
Consumer usage:
# Get order with lines expanded
GET /salesOrders({id})?$expand=salesOrderLines
# Get lines for a specific order
GET /salesOrders({id})/salesOrderLines
# Add a line to an order
POST /salesOrders({id})/salesOrderLines
{ "lineType": "Item", "itemNumber": "ITEM-001", "quantity": 10 }
Pattern 3: Bound Actions (Operate on Entity)
Bound actions trigger business logic on a specific entity:
// Inside the API page's actions area:
actions
{
area(Processing)
{
// POST /salesOrders({id})/Microsoft.NAV.post
action(post)
{
ApplicationArea = All;
Caption = 'Post';
trigger OnAction()
var
SalesPost: Codeunit "Sales-Post";
begin
Rec.TestField(Status, Rec.Status::Released);
SalesPost.Run(Rec);
end;
}
// POST /salesOrders({id})/Microsoft.NAV.release
action(release)
{
ApplicationArea = All;
Caption = 'Release';
trigger OnAction()
var
ReleaseSalesDoc: Codeunit "Release Sales Document";
begin
ReleaseSalesDoc.PerformManualRelease(Rec);
end;
}
// POST /salesOrders({id})/Microsoft.NAV.reopen
action(reopen)
{
ApplicationArea = All;
Caption = 'Reopen';
trigger OnAction()
var
ReleaseSalesDoc: Codeunit "Release Sales Document";
begin
ReleaseSalesDoc.PerformManualReopen(Rec);
end;
}
}
}
Consumer call:
POST /salesOrders({id})/Microsoft.NAV.post
Content-Type: application/json
Pattern 4: Unbound Actions (Standalone Operations)
Unbound actions are not tied to a specific entity — use a virtual/dummy source table:
page 50102 "Contoso Utility API"
{
APIVersion = 'v2.0';
APIPublisher = 'contoso';
APIGroup = 'utilities';
EntityName = 'utilityFunction';
EntitySetName = 'utilityFunctions';
PageType = API;
SourceTable = "Company Information"; // read-only singleton as base
SourceTableTemporary = true;
InsertAllowed = false;
ModifyAllowed = false;
DeleteAllowed = false;
layout
{
area(Content)
{
repeater(Group)
{
field(companyName; Rec.Name) { Editable = false; }
}
}
}
actions
{
area(Processing)
{
// POST /utilityFunctions/Microsoft.NAV.calculateShipping
action(calculateShipping)
{
ApplicationArea = All;
Caption = 'Calculate Shipping';
trigger OnAction()
var
ShippingMgt: Codeunit "Contoso Shipping Management";
Weight: Decimal;
DestCode: Code[20];
begin
Evaluate(Weight, GetActionContext().GetText('weight'));
DestCode := CopyStr(GetActionContext().GetText('destinationCode'), 1, 20);
SetActionResponse(CreateJsonResponse(
ShippingMgt.CalculateCost(Weight, DestCode)));
end;
}
}
}
}
When you need versioning/deprecation, webhooks, or trigger-level error handling, load references/api-advanced-patterns.md.
XML Documentation for Public Procedures
Any public procedure that other modules call carries XML doc comments. This covers API pages (above), and equally the library codeunits invoked by API logic or by other codeunits — anything outside the unit's own boundary.
/// <summary>
/// Evaluates whether the customer qualifies as VIP based on sales volume
/// and persists the result on Customer."VIP Customer".
/// </summary>
/// <param name="CustomerNo">The customer number to evaluate. Exits silently if blank or not found.</param>
procedure EvaluateCustomer(CustomerNo: Code[20])
begin
// ...
end;
<summary> (required) — what the procedure does and why a caller would invoke it.
<param name="..."> (required for each non-trivial parameter) — what value to pass and constraints.
<returns> (required when there is a return value) — what the value means.
local and internal procedures: doc is optional.
This surface is what IntelliSense presents to consumers and what AL's missing-documentation diagnostics flag.
Workflow
Step 1: Design API Contract
Before implementing, define:
- Resource model — entities, relationships, navigation properties
- Operations — which HTTP methods per resource (GET/POST/PATCH/DELETE)
- Actions — custom operations (bound: per entity, unbound: global)
- Filtering — which fields consumers can
$filter on (add corresponding keys)
- Versioning — initial version and deprecation plan
- Authentication — OAuth 2.0 scope, permission sets needed
Document in .github/plans/{req_name}.architecture.md or a dedicated API design section.
PAUSE — wait for user approval before implementing.
Step 2: Implement API Pages
- Create header API page (Pattern 1)
- Create subpage(s) for lines/children (Pattern 2)
- Add bound actions for entity operations (Pattern 3)
- Add unbound actions if needed (Pattern 4)
- Add error handling triggers (Pattern 7)
- Build:
al_build
Step 3: Optimize for Performance
Add keys for filterable fields:
tableextension 50100 "Contoso Sales Header Ext" extends "Sales Header"
{
keys
{
key(APICustomerDate; "Sell-to Customer No.", "Order Date") { }
key(APIStatus; Status, "Order Date") { }
}
}
Key OData query patterns:
- Projection:
?$select=number,customerNumber — reduces payload
- Filtering:
?$filter=customerNumber eq 'C00001' and orderDate ge 2025-01-01 — server-side
- Expansion:
?$expand=salesOrderLines — inline children
- Delta links: initial GET returns
@odata.deltaLink; subsequent call with $deltatoken returns only changes
- Pagination:
?$top=50&$skip=100
Step 4: Generate Permission Sets
Create role-based permission sets (full access + read-only) for the API pages. Follow skill-permissions.md for the hierarchy pattern. Minimum: one set granting X on all API pages + RIMD on table data, one read-only set with R only.
Step 5: Test
- Test CRUD operations (create, read, update, delete)
- Test bound actions (post, release, reopen)
- Test
$filter, $select, $expand query options
- Test error responses (missing required fields, blocked customer, invalid state)
- Test permission sets (read-only user cannot POST/PATCH/DELETE)
- Test
If-Match / ETag for optimistic concurrency on PATCH and DELETE
References
Constraints
- This skill covers API page design, implementation patterns, and OData conventions
- Do NOT modify base BC objects — create API pages as extensions only
- Do NOT expose internal implementation details (codeunit internals, temp tables) in API responses
- Do NOT skip
APIVersion — every API page MUST have an explicit version
- Do NOT create breaking changes on stable versions — use
beta for previewing changes, then promote
- Do NOT skip error handling —
OnInsertRecord, OnModifyRecord, OnDeleteRecord must validate
- Permission set hierarchy →
skill-permissions.md | Performance deep-dive → skill-performance.md | API testing → skill-testing.md