--- 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`