19. SDK Usage Guide
How another Shumoul module — a new ERP feature, or a future Shumoul.Saas.* service — should consume the WhatsApp Integration library. Everything below reflects the library's actual, current public surface (see §3 Packages); nothing here proposes new capability.
19.1 Dependency Injection and Service Registration
Add both packages as PackageReferences (never ProjectReference, per the platform's Dependency Law):
<PackageReference Include="Shumoul.WhatsAppIntegration.Abstractions" Version="1.0.1" />
<PackageReference Include="Shumoul.WhatsAppIntegration.Core" Version="1.0.1" />
Register everything with the library's own extension method, once, at startup:
services.AddWhatsAppIntegration(configuration);
This single call (Core/Extensions/WhatsAppIntegrationServiceCollectionExtensions.cs) does exactly three
things — no more:
services.Configure<WhatsAppCloudApiOptions>(config.GetSection(WhatsAppCloudApiOptions.SectionName));
services.AddHttpClient("WhatsAppCloudApi");
services.AddTransient<IWhatsAppService, WhatsAppCloudApiClient>();
services.AddTransient<IWhatsAppSignatureValidator, WhatsAppSignatureValidator>();
services.AddTransient<IWhatsAppWebhookParser, WhatsAppWebhookParser>();
After this call, inject IWhatsAppService, IWhatsAppWebhookParser, or IWhatsAppSignatureValidator into
any consuming service through ordinary constructor injection — no further setup is required for the
platform's own WhatsApp number.
19.2 Credential Override (Per-Tenant / Per-Call Credentials)
IWhatsAppService/WhatsAppCloudApiClient is registered once against the platform-wide
WhatsAppCloudApiOptions. If your module needs to send using a different set of credentials (e.g. a
tenant's own WhatsApp Business number, as the Communication module does), do not re-register the DI
service — construct a second, short-lived WhatsAppCloudApiClient instance directly, merging the
alternate credentials with the platform's BaseUrl/GraphApiVersion:
var overrideOptions = Options.Create(new WhatsAppCloudApiOptions
{
AccessToken = tenantSettings.AccessToken,
PhoneNumberId = tenantSettings.PhoneNumberId,
BusinessAccountId = tenantSettings.BusinessAccountId,
BaseUrl = platformOptions.Value.BaseUrl, // still centralized
GraphApiVersion = platformOptions.Value.GraphApiVersion, // still centralized
});
var client = new WhatsAppCloudApiClient(overrideOptions, httpClientFactory, logger);
await client.SendTemplateAsync(request);
This is exactly the pattern TenantWhatsAppSender uses (see
§2.4) — it requires zero changes to the
library and keeps BaseUrl/GraphApiVersion centralized even when credentials are overridden. Never
hardcode a Graph API version or base URL when doing this — always source them from the injected
IOptions<WhatsAppCloudApiOptions>.
19.3 Tenant Context
The library itself has no concept of a tenant — it is stateless and credential-agnostic beyond whatever
WhatsAppCloudApiOptions instance you hand it. Tenant resolution, tenant-scoped settings
(WhatsAppOperationalSettings), and multi-tenant data isolation are entirely the consuming module's
responsibility, exactly as with every other IAppSettings-based tenant setting on this platform (see
§4.3 Configuration).
Resolve tenant settings via IAppSettingService.GetAppSetting<WhatsAppOperationalSettings>() before
constructing an override client per §19.2 — never cache a constructed WhatsAppCloudApiClient across
requests for different tenants.
19.4 Media Handling
There is no outbound media-upload method on IWhatsAppService — attach media to an outbound message as a
publicly reachable URL inside a template component parameter (ImageUrl/DocumentUrl/VideoUrl on
WhatsAppTemplateComponentParameterDto), not as an uploaded binary. For inbound media, call
GetMediaMetaAsync(mediaId) for metadata alone, or DownloadMediaBytesAsync(mediaId) for the full binary
(which internally performs its own metadata lookup plus the binary download — two HTTP round trips). Always
proxy downloaded bytes through your own backend to the frontend; never expose the Meta CDN URL directly (see
§10.6).
19.5 Error Handling and Retries
There is no built-in resilience policy at the HTTP layer. services.AddHttpClient("WhatsAppCloudApi")
carries no Polly retry policy, timeout override, or circuit breaker — confirmed by source inspection (see
§18.5). A transient network
failure or a Meta rate-limit response is surfaced to your calling code exactly once, with no automatic
retry, unless your own module implements one.
Two ways ERP modules currently handle this:
- Fire-and-fail (Communication module, manual test endpoints): a single attempt; failure is recorded
(
CommunicationHistory.ErrorMessage, or aResult<T>.FailureAsyncmessage) and surfaced to the caller immediately. No retry. - Classify-then-queue (the generic Notification Framework dispatch path): a failed send's exception is
passed to
NotificationFailureClassifier.Classify(NotificationChannel.WhatsApp, exception), which pattern-matches the exception'sMessage/type name into aNotificationFailureType(RateLimit/InvalidRecipient/AuthenticationFailure/Timeout/NetworkFailure/Temporary) and queues a retry entry processed by the Notification Framework's generic, channel-agnostic retry pipeline (every 2 minutes). See §22 Meta Error Reference for exactly which Meta error signatures this classifier recognizes today.
If your module needs retry behavior and isn't going through the Notification Framework's dispatch pipeline,
you must implement it yourself — do not assume the library or IWhatsAppService will retry for you.
19.6 Webhook Registration
Webhook receipt is entirely the ERP's responsibility — the library only provides
IWhatsAppSignatureValidator/IWhatsAppWebhookParser as building blocks, it does not expose or manage an
HTTP endpoint itself. To wire up webhook receipt in a new host:
- Expose an
[AllowAnonymous]GET/POSTpair at a URL you register in the Meta App Dashboard (see §8.1–8.2). GET: comparehub.verify_tokenagainst your configuredWhatsAppCloudApiOptions.VerifyTokenand echohub.challengeon a match.POST: read the raw request body bytes (before any JSON model binding, since the signature is computed over the exact bytes Meta sent), callIWhatsAppSignatureValidator.Validate(bodyBytes, signatureHeader), reject with403on failure, otherwise callIWhatsAppWebhookParser.ParseAsync(rawPayloadString)and process the returnedInboundMessages/StatusUpdates.- Always return
200 OKonce the signature check passes, regardless of per-item processing outcomes (see §8.8) — this is a deliberate anti-retry-storm design, not an oversight, and should be preserved in any new host implementation.
19.7 What Not To Do
- Do not construct a raw
HttpClientyourself to call Meta directly — always go throughWhatsAppCloudApiClient(via DI or a manually-constructed override instance per §19.2). - Do not hardcode a Graph API version or base URL anywhere in a new consuming module — always read
WhatsAppCloudApiOptions. - Do not implement your own phone-number normalization for a WhatsApp send inside a new module — call
Shumoul.Application.Helpers.WhatsAppPhoneHelper.TryNormalizePhone(ERP-owned; the library has no equivalent and is not meant to gain one, per ADR-001). - Do not assume automatic retries exist anywhere below the Notification Framework's generic dispatch pipeline (§19.5) — verify your own module's failure-handling needs explicitly.
