Business Central Azure Functions (Part 2)
Neil Haddley • December 16, 2025
Creating the Business Central Extension
Creating the Business Central Extension

Azure Functions App used to generate Customer QR Code
project files
TEXT
1ALProjectQRCodeGenerator/ 2├── .vscode/ 3├── .alpackages/ 4├── .git/ 5├── .snapshots/ 6├── app.json 7├── Customer Card QR Extension.al 8├── Customer QR Code FactBox.al 9├── QR Code Helper.al 10├── QR Code Media.al 11└── Neil Haddley_ALProjectQRCodeGenerator_1.0.0.0.app
Summary: ALProjectQRCodeGenerator
Overview
A Business Central extension that automatically generates and displays QR codes for customer records using an Azure Function integration.
Architecture Flow
CODE
1Customer Card → FactBox → Helper → Azure Function → MediaSet Storage → Display
1. QR Code Media.al (Table 50100)
Purpose: Persistent storage for QR code images
AL
1table 50100 "QR Code Media" 2{ 3 fields 4 { 5 "Customer No." (Code[20]) // Primary key, links to Customer 6 "Media ID" (Media) // Legacy binary storage field 7 "QR Code Image" (MediaSet) // Active field for UI display 8 "Created DateTime" (DateTime) // Auto-timestamp on insert 9 } 10 11 trigger OnInsert() 12 { 13 "Created DateTime" := CurrentDateTime; // Auto-populate timestamp 14 } 15}
2. QR Code Helper.al (Codeunit 50100)
Purpose: Business logic for QR code generation and storage
CODE
11. Validate customer exists 22. Build QR text content (customer number + name) 33. Construct API URL with encoded parameters 44. Detect response format (base64 vs binary) 55. Call Azure Function via HttpClient 66. Convert response to MediaSet 77. Store in database 88. Return MediaId GUID
GetCustomerQRText() - Content Builder
- Retrieves company info
- Formats: CUSTOMER:[No.]|NAME:[Name]
- Commented option: Direct URL to BC customer card
StoreQRCodeImageFromBase64() - Base64 Handler
- Extracts base64 data from data:image/png;base64,... URI
- Uses Base64 Convert codeunit
- Converts to binary stream via Temp Blob
- Imports to MediaSet with .png filename
StoreQRCodeImage() - Binary Handler
- Direct stream import from HTTP response
- Used for APIs returning raw PNG bytes
IsBase64Response() - Format Detector
- Checks if URL contains 'azurewebsites'
- Returns true for Azure Functions (base64)
- Returns false for public APIs (binary)
EncodeUrl() - URL Encoding
- Uses Type Helper.UriEscapeDataString()
- Properly encodes special characters for query parameters
3. Customer QR Code FactBox.al (Page 50100)
Purpose: UI component displaying QR code in Customer Card
AL
1page 50100 "Customer QR Code FactBox" 2{ 3 PageType = CardPart; 4 SourceTable = "QR Code Media"; 5 6 layout { 7 field("QR Code Image") // MediaSet displays as image 8 label("Scan to view...") // User guidance text 9 } 10 11 trigger OnOpenPage() { 12 if Rec.IsEmpty() then 13 GenerateQRCode(); // Auto-generate if missing 14 } 15}
Logic:
- Checks if QR code exists on page open
- Calls GenerateQRCodeForCustomer() if missing
- Refreshes page to display new image
4. Customer Card QR Extension.al (PageExtension 50100)
Purpose: Integrates FactBox into standard Customer Card
AL
1pageextension 50100 extends "Customer Card" 2{ 3 layout { 4 addfirst(factboxes) { 5 part(QRCodeFactBox; "Customer QR Code FactBox") { 6 SubPageLink = "Customer No." = FIELD("No."); 7 } 8 } 9 } 10}
Integration:
- Adds FactBox to top of factboxes area
- Links FactBox to current customer via SubPageLink
- No code modifications to base page
5. app.json
Configuration:
- Dependencies: Base Application (27.0), System Application (27.0)
- Object Range: 50100-50149
- Runtime: 16.0 with NoImplicitWith
- Publisher: Neil Haddley
Technical Highlights
✅ Dual Format Support: Handles both base64 and binary image responses
✅ Auto-Generation: Creates QR codes on-demand when missing
✅ Clean Separation: Helper handles logic, FactBox handles UI
✅ Modern BC Patterns: Uses HttpClient (not deprecated Azure Functions codeunits)
✅ MediaSet Storage: Proper image handling for BC UI rendering
✅ URL Encoding: Safely handles special characters in customer data
Data Flow Example
CODE
1User opens Customer "C001" 2 ↓ 3FactBox checks QR Code Media for "C001" 4 ↓ 5Not found → Calls GenerateQRCodeForCustomer("C001") 6 ↓ 7Helper builds: "CUSTOMER:C001|NAME:Contoso Ltd" 8 ↓ 9Calls: https://azure-function.../api/QRCodeGenerator?text=CUSTOMER%3AC001... 10 ↓ 11Receives: data:image/png;base64,iVBORw0KGgoAAAA... 12 ↓ 13Converts base64 → binary stream → MediaSet 14 ↓ 15Stores in QR Code Media table 16 ↓ 17FactBox displays QR code image 18 ↓ 19User scans with phone → Sees customer info
app.json
JSON
1{ 2 "id": "d3c72ccb-3945-46b3-8e4e-b784aaf1bd69", 3 "name": "ALProjectQRCodeGenerator", 4 "publisher": "Neil Haddley", 5 "version": "1.0.0.0", 6 "brief": "", 7 "description": "", 8 "privacyStatement": "", 9 "EULA": "", 10 "help": "", 11 "url": "", 12 "logo": "", 13 "dependencies": [ 14 { 15 "id": "437dbf0e-84ff-417a-965d-ed2bb9650972", 16 "name": "Base Application", 17 "publisher": "Microsoft", 18 "version": "27.0.0.0" 19 }, 20 { 21 "id": "63ca2fa4-4f03-4f2b-a480-172fef340d3f", 22 "name": "System Application", 23 "publisher": "Microsoft", 24 "version": "27.0.0.0" 25 } 26 ], 27 "screenshots": [], 28 "platform": "1.0.0.0", 29 "application": "27.0.0.0", 30 "idRanges": [ 31 { 32 "from": 50100, 33 "to": 50149 34 } 35 ], 36 "resourceExposurePolicy": { 37 "allowDebugging": true, 38 "allowDownloadingSource": true, 39 "includeSourceInSymbolFile": true 40 }, 41 "runtime": "16.0", 42 "features": [ 43 "NoImplicitWith" 44 ] 45}
Customer Card QR Extension.al
CODE
1pageextension 50100 "Customer Card QR Extension" extends "Customer Card" 2{ 3 layout 4 { 5 addfirst(factboxes) 6 { 7 part(QRCodeFactBox; "Customer QR Code FactBox") 8 { 9 ApplicationArea = All; 10 SubPageLink = "Customer No." = FIELD("No."); 11 } 12 } 13 } 14}
Customer QR Code FactBox.al
CODE
1page 50100 "Customer QR Code FactBox" 2{ 3 PageType = CardPart; 4 SourceTable = "QR Code Media"; 5 Caption = 'Customer QR Code'; 6 7 layout 8 { 9 area(Content) 10 { 11 group(QRCodeGroup) 12 { 13 ShowCaption = false; 14 field(QRCodeImage; Rec."QR Code Image") 15 { 16 ApplicationArea = All; 17 ShowCaption = false; 18 Editable = false; 19 ToolTip = 'QR Code for customer'; 20 } 21 label(Description) 22 { 23 ApplicationArea = All; 24 Caption = 'Scan to view customer details'; 25 Style = Subordinate; 26 } 27 } 28 } 29 } 30 31 var 32 QRCodeHelper: Codeunit "QR Code Helper"; 33 34 trigger OnAfterGetRecord() 35 begin 36 // Record already loaded 37 end; 38 39 trigger OnOpenPage() 40 begin 41 // Generate QR code if it doesn't exist 42 if Rec.IsEmpty() then 43 GenerateQRCode(); 44 end; 45 46 local procedure GenerateQRCode() 47 var 48 Customer: Record Customer; 49 begin 50 if Customer.Get(Rec."Customer No.") then 51 QRCodeHelper.GenerateQRCodeForCustomer(Customer."No."); 52 53 // Refresh the page to show the new QR code 54 if Rec.Get(Rec."Customer No.") then; 55 end; 56}
QR Code Helper.al
CODE
1codeunit 50100 "QR Code Helper" 2{ 3 // This function generates a QR code for a customer and stores it 4 procedure GenerateQRCodeForCustomer(CustomerNo: Code[20]): Guid 5 var 6 Customer: Record Customer; 7 HttpClient: HttpClient; 8 HttpResponse: HttpResponseMessage; 9 ResponseText: Text; 10 InStr: InStream; 11 FunctionUrl: Text; 12 QRCodeText: Text; 13 ImageMediaId: Guid; 14 UseBase64: Boolean; 15 begin 16 // 1. Get the customer record 17 if not Customer.Get(CustomerNo) then 18 exit; 19 20 // 2. Build the text to encode in the QR code 21 QRCodeText := GetCustomerQRText(Customer); 22 23 // 3. Build the full URL with encoded query parameter 24 FunctionUrl := GetFunctionUrl() + EncodeUrl(QRCodeText); 25 UseBase64 := IsBase64Response(FunctionUrl); 26 27 // 4. Call the API to generate QR code 28 if not HttpClient.Get(FunctionUrl, HttpResponse) then 29 Error('Failed to call QR code generation service'); 30 31 if not HttpResponse.IsSuccessStatusCode() then 32 Error('Failed to generate QR code. Status: %1', HttpResponse.HttpStatusCode()); 33 34 // 5. Get the response content 35 if UseBase64 then begin 36 if not HttpResponse.Content.ReadAs(ResponseText) then 37 Error('Failed to read response content'); 38 ImageMediaId := StoreQRCodeImageFromBase64(CustomerNo, ResponseText); 39 end else begin 40 if not HttpResponse.Content.ReadAs(InStr) then 41 Error('Failed to read response content'); 42 ImageMediaId := StoreQRCodeImage(CustomerNo, InStr); 43 end; 44 45 exit(ImageMediaId); 46 end; 47 48 local procedure EncodeUrl(TextToEncode: Text): Text 49 var 50 TypeHelper: Codeunit "Type Helper"; 51 begin 52 exit(TypeHelper.UriEscapeDataString(TextToEncode)); 53 end; 54 55 local procedure GetCustomerQRText(Customer: Record Customer): Text 56 var 57 CompanyInfo: Record "Company Information"; 58 BaseUrl: Text; 59 begin 60 // Customize this based on what you want in the QR code 61 if CompanyInfo.Get() then 62 BaseUrl := CompanyInfo."Home Page"; // Or your Business Central base URL 63 64 // Example 1: URL to the customer card in Business Central 65 // exit(StrSubstNo('%1?page=21&bookmark=Customer.''No.''=%2', BaseUrl, Customer."No.")); 66 67 // Example 2: Simple customer information 68 exit(StrSubstNo('CUSTOMER:%1|NAME:%2', Customer."No.", Customer.Name)); 69 end; 70 71 local procedure StoreQRCodeImageFromBase64(CustomerNo: Code[20]; Base64DataUri: Text): Guid 72 var 73 QRCodeMedia: Record "QR Code Media"; 74 Base64Convert: Codeunit "Base64 Convert"; 75 TempBlob: Codeunit "Temp Blob"; 76 InStr: InStream; 77 OutStr: OutStream; 78 Base64Data: Text; 79 PrefixPos: Integer; 80 begin 81 // Check if record already exists 82 if QRCodeMedia.Get(CustomerNo) then 83 QRCodeMedia.Delete(); 84 85 // Extract base64 data from data URI 86 // Format: data:image/png;base64,iVBORw0KGgo... 87 PrefixPos := StrPos(Base64DataUri, ','); 88 if PrefixPos > 0 then 89 Base64Data := CopyStr(Base64DataUri, PrefixPos + 1) 90 else 91 Base64Data := Base64DataUri; 92 93 // Convert base64 to stream 94 TempBlob.CreateOutStream(OutStr); 95 Base64Convert.FromBase64(Base64Data, OutStr); 96 TempBlob.CreateInStream(InStr); 97 98 // Create a new record 99 QRCodeMedia.Init(); 100 QRCodeMedia."Customer No." := CustomerNo; 101 102 // Import into MediaSet field 103 QRCodeMedia."QR Code Image".ImportStream(InStr, 'QRCode.png'); 104 QRCodeMedia.Insert(true); 105 106 exit(QRCodeMedia."QR Code Image".MediaId()); 107 end; 108 109 local procedure StoreQRCodeImage(CustomerNo: Code[20]; InStr: InStream): Guid 110 var 111 QRCodeMedia: Record "QR Code Media"; 112 begin 113 // Check if record already exists 114 if QRCodeMedia.Get(CustomerNo) then 115 QRCodeMedia.Delete(); 116 117 // Create a new record in our custom media table 118 QRCodeMedia.Init(); 119 QRCodeMedia."Customer No." := CustomerNo; 120 QRCodeMedia."QR Code Image".ImportStream(InStr, 'QRCode.png'); 121 QRCodeMedia.Insert(true); 122 123 exit(QRCodeMedia."QR Code Image".MediaId()); 124 end; 125 126 local procedure GetFunctionUrl(): Text 127 begin 128 129 // Your Azure Function that returns base64 data URI 130 exit('https://qrcode-generator-function-e5c2d8dwcabsbbfe.eastus-01.azurewebsites.net/api/QRCodeGenerator?text='); 131 132 133 // Alternative: Using public QR API (returns binary PNG directly) 134 //exit('https://api.qrserver.com/v1/create-qr-code/?size=200x200&data='); 135 end; 136 137 local procedure IsBase64Response(Url: Text): Boolean 138 begin 139 // Returns true if the configured API returns base64 data URI 140 // Azure Functions typically return base64, public APIs return binary 141 exit(StrPos(Url, 'azurewebsites') > 0); 142 end; 143 144 local procedure GetFunctionKey(): Text 145 begin 146 // IMPORTANT: Use Isolated Storage or Azure Key Vault integration in production 147 exit('your-actual-function-key-here'); 148 end; 149}
QR Code Media.al
CODE
1table 50100 "QR Code Media" 2{ 3 DataClassification = ToBeClassified; 4 5 fields 6 { 7 field(1; "Customer No."; Code[20]) 8 { 9 TableRelation = Customer; 10 } 11 field(2; "Media ID"; Media) 12 { 13 // This field stores the actual image 14 Caption = 'QR Code Image'; 15 } 16 field(3; "QR Code Image"; MediaSet) 17 { 18 Caption = 'QR Code'; 19 } 20 field(4; "Created DateTime"; DateTime) 21 { 22 Editable = false; 23 } 24 } 25 26 keys 27 { 28 key(PK; "Customer No.") 29 { 30 Clustered = true; 31 } 32 } 33 34 trigger OnInsert() 35 begin 36 "Created DateTime" := CurrentDateTime; 37 end; 38}
The error message "The request was blocked by the runtime to prevent accidental use of production services" indicates that outbound HTTP calls are blocked by default in your Business Central environment, typically a sandbox or a copied production environment. To resolve this, you need to manually enable the setting for your extension.

Allow HttpClient Requests