---
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)
```al
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:
```al
// 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.");
}
```
```al
// 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:**
```http
# 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:
```al
// 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:**
```http
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:
```al
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.
```al
///
/// Evaluates whether the customer qualifies as VIP based on sales volume
/// and persists the result on Customer."VIP Customer".
///
/// The customer number to evaluate. Exits silently if blank or not found.
procedure EvaluateCustomer(CustomerNo: Code[20])
begin
// ...
end;
```
- `` (required) — what the procedure does and why a caller would invoke it.
- `` (required for each non-trivial parameter) — what value to pass and constraints.
- `` (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:
1. **Resource model** — entities, relationships, navigation properties
2. **Operations** — which HTTP methods per resource (GET/POST/PATCH/DELETE)
3. **Actions** — custom operations (bound: per entity, unbound: global)
4. **Filtering** — which fields consumers can `$filter` on (add corresponding keys)
5. **Versioning** — initial version and deprecation plan
6. **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
1. Create header API page (Pattern 1)
2. Create subpage(s) for lines/children (Pattern 2)
3. Add bound actions for entity operations (Pattern 3)
4. Add unbound actions if needed (Pattern 4)
5. Add error handling triggers (Pattern 7)
6. Build: `al_build`
### Step 3: Optimize for Performance
Add keys for filterable fields:
```al
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
- [API Page Type — Microsoft Docs](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-api-pagetype)
- [API v2.0 Standard Endpoints](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/)
- [OData Query Parameters](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-connect-apps-filtering)
- [Custom APIs](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-develop-custom-api)
- [Webhook Subscriptions](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-subscriptions)
- [API Performance](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/performance/performance-developer#writing-efficient-api-pages)
## 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`