Every integration that leaves Business Central goes through HttpClient. The call itself is six lines; doing it safely is the rest. Headers and authentication, timeouts, reading the status before the body, retries with backoff, where secrets live, telemetry, and the TryFunction boundary that keeps a failed call from taking the transaction with it.
Outbound HTTP from AL is simple to write and easy to write badly. This is the shape we use for every outbound call, with the reasons.
[TryFunction]
local procedure TrySend(Url: Text; Body: Text; var ResponseText: Text; var StatusCode: Integer)
var
Client: HttpClient;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
Content: HttpContent;
Headers: HttpHeaders;
begin
Content.WriteFrom(Body);
Content.GetHeaders(Headers);
Headers.Remove('Content-Type');
Headers.Add('Content-Type', 'application/json');
Request.Content := Content;
Request.SetRequestUri(Url);
Request.Method := 'POST';
Request.GetHeaders(Headers);
Headers.Add('Authorization', StrSubstNo(BearerTok, GetToken()));
Client.Timeout := 30000;
if not Client.Send(Request, Response) then
Error(NoResponseErr);
StatusCode := Response.HttpStatusCode();
Response.Content().ReadAs(ResponseText);
end;
Header names and the bearer prefix are locked labels in a real codebase. Note the content-type must be set on the content's headers, not the request's; it is the most common "415 Unsupported Media Type" cause.
procedure Send(Shipment: Record "CON Courier Shipment")
var
ResponseText: Text;
Status: Integer;
Attempt: Integer;
begin
repeat
Attempt += 1;
if TrySend(Setup.GetEndpoint(), BuildBody(Shipment), ResponseText, Status) then
case Status of
200, 201:
begin
StoreResult(Shipment, ResponseText);
LogOk(Shipment, Status);
exit;
end;
429, 500, 502, 503, 504:
Sleep(Attempt * 2000); // back off, then loop
else
ErrorWithBody(Status, ResponseText); // 4xx: do not retry
end
else
LogFailure(Shipment, GetLastErrorText());
until Attempt >= 3;
Error(SendFailedErr, Shipment."No.");
end;
The TryFunction isolates the transport failure; the caller decides what a status means. Retry only on 429 and 5xx. Never retry a 4xx. Make the operation idempotent on the other side (send your reference; the API should treat a repeat as the same request).
API keys and client secrets are not in a setup table field. Online, use Codeunit "Azure Key Vault" via keyVaultUrls in app.json (for ISVs) or, for a per-tenant extension, Isolated Storage (IsolatedStorage.Set/Get with DataScope::Module), entered through a page with a masked field and never displayed back. Tokens are cached in Isolated Storage with their expiry and refreshed before it.
Log start, end, status and duration with a correlation ID you also send as a header, so the other side's logs match yours (telemetry). Long or batched sends run from the job queue, not from a user's session, so a slow API does not hold a posting transaction.
Business Central online allows outbound HTTPS only, and the sandbox has a setting that blocks outbound calls by default after a copy (Allow HttpClient Requests per extension in Extension Management). Document it, because "the integration stopped after the refresh" is that switch.