Branded Document Delivery and Open Tracking
Per-template branded email delivery for invoices, statements and orders with open/read tracking and bounce handling.
- Current version: v0.1.1.0
- AL prefix: BRAND
- Object range: 80900-80999
- Business Central: 26.0.0.0
- Publisher: Amplio Solutions
15 — Branded Document Delivery & Open Tracking
A Business Central 26.x extension that sends per-template branded HTML emails for sales documents (invoices, credit memos, orders, quotes, statements) via SendGrid, embeds an open-tracking pixel, ingests SendGrid bounce/open events through a token-secured webhook, and logs every send on the Posted Sales Invoice as a FactBox.
- Publisher: Amplio Solutions
- Version: 0.1.1.0
- Application / Platform: 26.0.0.0
- Object range: 80900–80999 (
AMPprefix) - Country: GB
- Runtime: 13.0
- Target: Cloud (works in OnPrem sandboxes too)
What this extension does
Business Central's standard Email module ships an Account+Connector model and a generic body composer. This extension does not replace either. Instead it:
- Adds a Setup table (singleton) holding the SendGrid API base URL, sender defaults, public tracking base URL, and toggles for tracking-pixel embedding and test-recipient redirection. Secrets (SendGrid API key, inbound webhook token) live in Isolated Storage — never in the table.
- Adds an Email Template table keyed by code, with a one-default-per-document-type rule, an HTML Blob body, optional sender override, and a placeholder
Customer No.field reserved for v2 per-customer inheritance. - Adds a SendGrid HttpClient wrapper codeunit that POSTs to
/v3/mail/sendwith the rendered HTML body and acustom_args.tracking_idGUID, so the bounce/open events can be correlated back. - Adds a Branded Email Sender codeunit that resolves the template, builds the customer + document merge dictionary, renders subject/body, optionally appends a 1×1 tracking pixel, calls the wrapper, and writes an entry into the Email Send Log with status (Pending → Sent / Failed → Delivered / Opened / Bounced / Spam).
- Adds a Webhook Handler codeunit that validates a shared bearer token (constant-time compare against Isolated Storage), parses a SendGrid event-webhook JSON array, and writes both an Email Tracking Event row and a status update on the corresponding Send Log row.
- Exposes a token-secured API page (
AMP Webhook Endpoint, page 80940) with bound actionsprocessSendGridEventsandrecordPixelHit. The token is supplied as an action parameter (BC API pages cannot read HTTP request headers from AL). - Subscribes to
Codeunit 8888 "Email"."OnAfterSendEmail"to log all platform-driven sends alongside the SendGrid-driven ones. Body substitution for platform sends is deferred — the platform's pre-send hooks are internal in BC 26 and not subscribable from third-party extensions. - Extends the Customer Card with a placeholder default-template field and a navigation action to the per-customer send log.
- Extends the Posted Sales Invoice with a delivery FactBox and a "Send Branded Email" action.
Object map
| Range | Kind | Examples |
|---|---|---|
| 80900–80903 | Tables | Email Setup, Email Template, Email Send Log, Email Tracking Event |
| 80910–80912 | Enums | Email Document Type, Email Send Status, Email Tracking Event Type |
| 80920–80925 | Codeunits | Email Template Mgt, SendGrid Wrapper, Branded Email Sender, Email Event Subscriber, Webhook Handler, Install |
| 80930–80939 | Pages | Email Setup, Template List/Card, Send Log, Tracking Events, Delivery FactBox, Secret Input |
| 80940 | API Page | Webhook Endpoint (/api/amp/brandedEmail/v1.0/...) |
| 80950 | TableExtension | Customer (Default Email Template field) |
| 80950–80951 | PageExtensions | Customer Card, Posted Sales Invoice |
| 80990–80991 | Permission Sets | Full (AMP Branded Email) + read-only |
Folder layout
15-Branded-Email-Delivery/
├── app.json
├── .editorconfig
├── .gitignore
├── README.md
├── .vscode/
│ └── launch.json
└── src/
├── Codeunits/
├── Enums/
├── PageExtensions/
├── Pages/
├── Permissions/
├── TableExtensions/
└── Tables/
Dependencies
Pinned in app.json:
| App | Publisher | Version |
|---|---|---|
| Base Application | Microsoft | 26.0.0.0 |
| System Application | Microsoft | 26.0.0.0 |
The standard Email module (Codeunit 8888 "Email", Codeunit 8904 "Email Message") lives in System Application, so the dependency above covers it. No SendGrid AL dependency — the wrapper uses the BC HttpClient directly.
What's complete
- AL scaffold compiles against BC 26 symbols (Base + System Application).
- Setup table + page with two Isolated-Storage secrets (SendGrid API key, webhook token), set/clear actions, masked secret-input modal.
- Email Template table + Card/List with merge-field-aware Subject (
Text[250]) and HTML Blob body, "Default for Document Type" enforcement, and HTML import-from-file action. - Email Send Log table + page with status traffic-lighting (Favorable / Unfavorable / Standard).
- Email Tracking Event table + list with event-type styling.
- Email Document Type enum (Sales Invoice, Credit Memo, Quote, Order Confirmation, Statement, Reminder, Other) — extensible for v2.
- Email Send Status + Tracking Event Type enums.
AMP Email Template Mgtcodeunit: resolver (default-per-doc-type, MVP ignores per-customer), Subject + HTML body merge-field renderer, customer + document merge-field builders, tracking-pixel HTML builder.AMP SendGrid Wrappercodeunit: HttpClient POST to SendGrid Mail Send v3, JSON payload withtracking_settings.open_tracking.enable=false(we use our own pixel) andclick_tracking.enable=false(deferred to v2),custom_args.tracking_idround-trip, X-Message-Id capture, error capture into the Send Log.AMP Branded Email Sender: end-to-end orchestration forSendForPostedSalesInvoice— resolve template, build merge fields, render, embed pixel, send, log result.AMP Email Event Subscriber:OnAfterSendEmailsubscriber on Codeunit 8888 (logging only).AMP Webhook Handler: token validation (constant-time), SendGrid event-array parser, per-event mapping into the tracking-event table and status update on the corresponding Send Log row, pixel-hit recorder.AMP Webhook Endpoint(page 80940, PageType=API): bound actionsprocessSendGridEvents(token, payload)andrecordPixelHit(trackingId, userAgent, ipAddress).- Customer Card extension: placeholder default-template field + navigation to per-customer send log + open-templates action.
- Posted Sales Invoice extension: Delivery FactBox + "Send Branded Email" + "Branded Email Log" actions.
- TableExtension on Customer (field 80950 "AMP Default Email Template", reserved for v2).
- Two permission sets: full RIMD (
AMP Branded Email) + read-only. - Install codeunit that ensures the Setup singleton row exists on first publish.
What Lee needs to ship
Below is the punch list for going from this MVP scaffold to a production-running deployment.
Verify before publish
- Symbol compile. From VS Code with the AL Language extension installed:
AL: Download symbolsagainst your sandbox, thenAL: Package(orCtrl+F5). The scaffold is intended to compile clean against BC 26 — if any object reports a missing symbol, capture the message before working around it (the most likely culprit is theEmail/Email Messagecodeunit being moved between System Application versions). OnAfterSendEmailevent signature. Confirm in your downloaded BC 26 symbols thatCodeunit::Email'sOnAfterSendEmailmatches(MessageId: Guid; Status: Boolean)— Microsoft has tweaked this signature historically. If it differs, updateAMPEmailEventSubscriber.Codeunit.al.launch.jsonstartup object. The default startup object is currently page 22 (Customer List) on both launch profiles. If you want F5 to land directly on the AMP Email Setup page, changestartupObjectIdto80930in.vscode/launch.json(the file is editor-protected in this session so it wasn't auto-updated).- Cloud Sandbox name. Edit
environmentNamein.vscode/launch.json(currentlyAmplioDev).
Configuration / data seeding
- SendGrid account. Provision a SendGrid account, verify a sender domain (so the
Default From Emailpasses SendGrid's domain authentication), and create an API key with at leastmail.sendandtrackingscopes. - Open Setup, populate. Run page 80930 and set:
Enabled = true,Default From Email,Default From Name,Public Tracking Base URL(HTTPS base of the BC API — typicallyhttps://api.businesscentral.dynamics.com/v2.0/<tenant>/<env>/). Then click Set SendGrid API Key and Set Webhook Token. - Configure SendGrid Event Webhook. In SendGrid → Settings → Mail Settings → Event Webhook:
- HTTP Post URL:
<Public Tracking Base URL>api/amp/brandedEmail/v1.0/companies(<companyId>)/webhookEndpoints(<systemId>)/Microsoft.NAV.processSendGridEvents<companyId>is the BC company GUID (visible on the Companies API).<systemId>is the systemId of the AMP Email Setup row — fetch it with aGETagainstwebhookEndpointsfirst.
- Body template: SendGrid does not natively wrap the events array with a custom JSON envelope, but it does support a static "Custom Headers" field. However, BC API pages cannot read HTTP headers from AL. Workaround for production: put a small Azure Function / APIM policy in front that (a) reads the SendGrid signature header, (b) wraps the body as
{"token":"<value>","payload":<events>}, then (c) forwards it to the BC bound action with the action's expected parameter shape. This shim is the right v1.1 work item. - Select events:
delivered,open,bounce,dropped,spamreport, optionallyprocessedanddeferred.
- HTTP Post URL:
- Tracking pixel route. The pixel URL embedded in outgoing emails points at the BC API endpoint, but BC API page actions return JSON, not 1×1 GIFs. Same Azure Function / APIM shim: GET on
/track/pixel/<trackingId>→ callrecordPixelHit→ returnimage/gif1×1 transparent. Until that shim exists, either turn offEmbed Tracking Pixelin Setup and rely on SendGrid'sopenevents for opens (re-enable open-tracking inAMPSendGridWrapper.BuildPayloadif you go this route), or deploy the shim. The recommended MVP path is "rely on SendGrid open events" because it's zero infrastructure. - Seed at least one Email Template. Open the AMP Email Template list, create one keyed e.g.
INV-DEFAULT, set Document Type = Sales Invoice, Default For Document Type = ✓, Enabled = ✓. Use Import HTML from File to load a designed HTML body, or paste inline. Available merge fields:%CustomerNo%,%CustomerName%,%CustomerContact%,%CustomerEmail%,%CompanyName%,%CompanyAddress%,%CompanyCity%,%CompanyPhone%,%CompanyEmail%,%DocumentNo%,%DocumentDate%,%DocumentAmount%,%DocumentCurrency%. - Permission assignment. Assign
AMP Branded Emailto the AR / billing team andAMP Branded Email ReadOnlyto viewers. Both ship as Assignable permission sets.
Deferred to v2 (intentionally out of MVP)
- Per-customer template inheritance. Field
Customer No.on the template +AMP Default Email Templateon the Customer card are scaffolded but the resolver inResolveTemplate()ignores them. The lookup signature already takesCustomerNoso the v2 change is local to the codeunit. - Click tracking. The SendGrid wrapper sends with
click_tracking.enable=false. Re-enabling it requires a body-rewrite step at SendGrid's end and ingestingclickevents into the existing tracking-event table (the enum value already exists). - Body substitution for platform-sent emails. The MVP's
OnAfterSendEmailsubscriber only logs — it does not rewrite the body. A v2 option is to send via the standard Email module with a custom Email Connector that calls our SendGrid wrapper; that gives the standard "Send-with-attachment" UX plus the branded body in one path. - Statement / Reminder dispatch entry points.
SendForPostedSalesInvoiceis the only orchestration procedure today. Mirror proceduresSendForCustomerStatement,SendForPostedSalesCrMemo,SendForReminderare the right v2 expansion (each uses the same template-resolve / merge-fields / wrapper plumbing). - Bounce-driven Customer flagging. When a customer bounces, the Send Log records it but the Customer Card doesn't reflect "email is undeliverable". A v2 enhancement is a flow-field count of recent bounces on the Customer.
- Job Queue / retry. No automatic retry on
FailedorDeferred. A v1.1 batch job that re-attempts entries withStatus = FailedandHTTP Status Code in (429, 5xx)is the right next step.
Build & deploy
From VS Code with the AL Language extension installed:
- Open the
15-Branded-Email-Deliveryfolder. AL: Download symbolsagainst your target sandbox.- Press F5 with the BC: Cloud Sandbox profile selected (edit
environmentNamefirst), or BC: Local Sandbox (Docker) for OnPrem. - The Customer List opens by default — search for "AMP Email Setup" or change
startupObjectIdto80930for direct landing on the setup page.
Smoke test path (manual)
- Publish the extension; open AMP Email Setup, set everything per Configuration step 6 above.
- Open AMP Email Templates, create
INV-DEFAULT(Sales Invoice, Default = ✓), paste an HTML body containing%CustomerName%and%DocumentNo%. - Set the Test Recipient Override in Setup to your own email (so no real customer is touched).
- Open any Posted Sales Invoice, click Branded Email → Send Branded Email.
- You should receive an email at the override address; the FactBox flips from empty → one row with Status
Sent. - After SendGrid processes the message (usually < 30 s), the webhook (once wired up) flips Status to
Delivered. Open the message in a mail client; the open event flips Status toOpenedand increments Open Count. - Force a bounce by setting Test Recipient Override to
bounce@simulator.amazonses.com(or SendGrid's equivalent invalid-mailbox simulator), repeat — Status flips toBouncedwith a populatedBounce Reason.