A complete, compilable example that validates sales order addresses from an external system without showing the user interface.
Why this needs its own code
The app validates Sales Header and Ship-to Address automatically, but only on modify. An order that arrives through a web service call with a complete address already in place, and that is never modified afterwards, is not covered by that automatic check. Call the public API yourself for this case.
What it does
The example subscribes to two standard table events and reacts only when a record was just inserted:
On Sales Header, it validates the sell-to address with the buffer overload of Address Validation API, because Sales Header has no Validate(Record) overload of its own.
On Ship-to Address, it validates the record directly with the Validate(Record) overload, because that table is one of the six the API publishes an overload for.
Both paths run with SetHideAddressSelection turned on and without raising an error to a user who is not there to see it - a failed validation is logged and the order is kept with the address it arrived with.
Code
codeunit 50100 "PTE External Order Addr. Val."{ [EventSubscriber(ObjectType::Table, Database::"Sales Header", 'OnAfterInsertEvent', '', false, false)] local procedure ValidateAddressOnAfterInsertSalesHeader(var Rec: Record "Sales Header"; RunTrigger: Boolean) var addressValidation: Codeunit "bdev.Address Validation API"; begin if (Rec."Document Type" <> Rec."Document Type"::Order) then exit; if (not addressValidation.AddressValidationEnabled(Database::"Sales Header")) then exit; addressValidation.SetHideAddressSelection(true); ValidateSellToAddress(Rec, addressValidation); end; local procedure ValidateSellToAddress(var salesHeader: Record "Sales Header"; var addressValidation: Codeunit "bdev.Address Validation API") var address, validatedAddress : Record "bdev.Address Validation Buffer" temporary; begin if (salesHeader."Sell-to Address" = '') then exit; if ((salesHeader."Sell-to City" = '') and (salesHeader."Sell-to Post Code" = '')) then exit; address.Init(); address."Primary Key" := CreateGuid(); address.Address := salesHeader."Sell-to Address"; address."Address 2" := salesHeader."Sell-to Address 2"; address."Post Code" := salesHeader."Sell-to Post Code"; address.City := salesHeader."Sell-to City"; address.County := salesHeader."Sell-to County"; address."Country/Region Code" := salesHeader."Sell-to Country/Region Code"; address."Source Type" := Database::"Sales Header"; address."Source System ID" := salesHeader.SystemId; if (not addressValidation.Validate(address, validatedAddress)) then begin LogValidationFailure(salesHeader."No.", 'Sell-to', GetLastErrorText()); exit; end; if (validatedAddress.Address = '') then exit; salesHeader."Sell-to Address" := validatedAddress.Address; salesHeader."Sell-to Post Code" := validatedAddress."Post Code"; salesHeader."Sell-to City" := validatedAddress.City; salesHeader."Sell-to County" := validatedAddress.County; salesHeader.Modify(true); end; [EventSubscriber(ObjectType::Table, Database::"Ship-to Address", 'OnAfterInsertEvent', '', false, false)] local procedure ValidateAddressOnAfterInsertShipToAddress(var Rec: Record "Ship-to Address"; RunTrigger: Boolean) var addressValidation: Codeunit "bdev.Address Validation API"; begin if (not addressValidation.AddressValidationEnabled(Database::"Ship-to Address")) then exit; addressValidation.SetHideAddressSelection(true); if (not TryValidateShipToAddress(Rec, addressValidation)) then LogValidationFailure(Rec."Customer No.", Rec.Code, GetLastErrorText()); end; [TryFunction] local procedure TryValidateShipToAddress(var shipToAddress: Record "Ship-to Address"; var addressValidation: Codeunit "bdev.Address Validation API") begin addressValidation.Validate(shipToAddress); end; local procedure LogValidationFailure(documentOrCustomerNo: Code[20]; addressPart: Text; errorText: Text) begin Session.LogMessage('PTE001', StrSubstNo(ValidationFailedTxt, addressPart, documentOrCustomerNo, errorText), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', 'PTE Address Validation'); end; var ValidationFailedTxt: Label '%1 address validation for %2 failed: %3', Locked = true;}
Why the two tables need different code
Address Validation API publishes Validate(Record) only for Contact, Customer, Vendor, Alternative Address, Order Address and Ship-To Address. Sales Header is not one of them, even though the app validates it automatically on modify - so the example builds a bdev.Address Validation Buffer from the sell-to fields by hand and uses the buffer overload, which is a [TryFunction] and never raises.
Validate(Record) for Ship-to Address is not a try function: a failed validation raises an error. Wrapping the call in a local [TryFunction] catches it before it reaches the caller, so the insert that triggered the subscriber still succeeds.
AddressValidationEnabled only answers for the app's own tables
AddressValidationEnabled(Database::"Sales Header") and AddressValidationEnabled(Database::"Ship-to Address") answer according to the setup fields the app itself evaluates for those tables - they are a convenient way to respect the tenant's own on/off switch and scope setting before calling the API, not a general-purpose check for a table of your own.