Skip to main content
Version: Latest

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:

  1. Fire-and-fail (Communication module, manual test endpoints): a single attempt; failure is recorded (CommunicationHistory.ErrorMessage, or a Result<T>.FailureAsync message) and surfaced to the caller immediately. No retry.
  2. 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's Message/type name into a NotificationFailureType (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:

  1. Expose an [AllowAnonymous] GET/POST pair at a URL you register in the Meta App Dashboard (see §8.1–8.2).
  2. GET: compare hub.verify_token against your configured WhatsAppCloudApiOptions.VerifyToken and echo hub.challenge on a match.
  3. POST: read the raw request body bytes (before any JSON model binding, since the signature is computed over the exact bytes Meta sent), call IWhatsAppSignatureValidator.Validate(bodyBytes, signatureHeader), reject with 403 on failure, otherwise call IWhatsAppWebhookParser.ParseAsync(rawPayloadString) and process the returned InboundMessages/StatusUpdates.
  4. Always return 200 OK once 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 HttpClient yourself to call Meta directly — always go through WhatsAppCloudApiClient (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.