Webority.Ads 0.1.2

dotnet add package Webority.Ads --version 0.1.2
                    
NuGet\Install-Package Webority.Ads -Version 0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Webority.Ads" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Webority.Ads" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Webority.Ads" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Webority.Ads --version 0.1.2
                    
#r "nuget: Webority.Ads, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Webority.Ads@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Webority.Ads&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Webority.Ads&version=0.1.2
                    
Install as a Cake Tool

Webority.Ads

Ad platform integration for Webority products, published as NuGet. Two halves in one family:

  • Conversions out. A product queues "this subject reached this conversion" once; the package writes one AdConversion row per configured platform in the product's own transaction, and a dispatcher the product hosts sends each row to Google Ads, Meta or LinkedIn and settles it.
  • Leads in. Verified webhook endpoints for Meta Lead Ads, Google Ads lead forms and LinkedIn Lead Gen Forms record each lead as an AdLead (a redelivery is a no-op) and hand it to the product's IAdLeadHandler off the request.

The package never owns a DbContext. Its entities live in the product's context, mapped by ApplyWeborityAds(), with the tables created from the SQL the package ships.

Proprietary: for Webority internal use only (see LICENSE).

Packages

Package What it carries
Webority.Ads AdConversion, AdLead, IAdConversionQueue, AdConversionDispatcher, AdLeadIntake, IAdPlatformConversionClient, IAdLeadHandler, IAdLeadDetailsSource, hashing, EF mappings, Schema/*.sql, AddWeborityAds<TContext>()
Webority.Ads.Google Data Manager offline click conversions (service-account token), Google Ads lead form parsing and key check, AddGoogleAds()
Webority.Ads.Meta Conversions API for CRM, Lead Ads Graph retrieval, webhook signature and handshake checks, AddMetaAds()
Webority.Ads.LinkedIn Conversions API events, Lead Gen Form retrieval through the Lead Sync API, webhook signature and challenge checks, AddLinkedInAds()
Webority.Ads.AspNetCore AddAdLeadWebhooks() and MapAdLeadWebhooks(): the lead endpoints and the background intake worker
Webority.Ads.Testing FakeAdPlatformConversionClient, FakeAdLeadDetailsSource, RecordingAdLeadHandler, AdFixtures (payloads, and Meta and LinkedIn signers)

Webority.Ads references Webority.Analytics at exactly 0.5.0 for MarketingTouch.

Wiring a product

// Startup.ConfigureServices
services.AddDbContext<AppDbContext>(...);
services.AddDbContextFactory<AppDbContext>(...);   // the dispatcher and the intake write through their own contexts

services.AddWeborityAds<AppDbContext>(_configuration)
    .AddGoogleAds()          // registers whatever Ads:Google / Ads:GoogleLeadForms carry
    .AddMetaAds()            // registers whatever Ads:Meta / Ads:MetaLeadAds carry
    .AddLinkedInAds()        // registers whatever Ads:LinkedIn / Ads:LinkedInLeadForms carry
    .AddAdLeadWebhooks();    // only on the head that receives lead webhooks

services.AddScoped<IAdLeadHandler, ProductLeadHandler>();   // required once AddAdLeadWebhooks is called

// Startup.Configure, inside UseEndpoints
endpoints.MapAdLeadWebhooks();   // GET+POST /webhooks/ads/meta, POST /webhooks/ads/google, GET+POST /webhooks/ads/linkedin

// AppDbContext.OnModelCreating
modelBuilder.ApplyWeborityAds();

Copy content/schema/AdConversion.sql and content/schema/AdLead.sql from the package into the product's Scripts/ (idempotent, guarded).

Configuration

{
  "Ads": {
    "DefaultPhoneCountryCode": "91",
    "Google": {
      "CustomerId": "7239470170",
      "ServiceAccountEmail": "ads@project.iam.gserviceaccount.com",
      "ServiceAccountPrivateKey": "<Key Vault>",
      "ConversionActions": { "signup_completed": "1234567890" }
    },
    "Meta": {
      "DatasetId": "123456789",
      "AccessToken": "<Key Vault>",
      "LeadEventSource": "Product name",
      "ConversionActions": { "signup_completed": "Qualified lead" }
    },
    "LinkedIn": {
      "AccessToken": "<Key Vault>",
      "ConversionActions": { "signup_completed": "104500001" }
    },
    "GoogleLeadForms": { "WebhookKey": "<Key Vault>" },
    "MetaLeadAds": { "AppSecret": "<Key Vault>", "VerifyToken": "<Key Vault>", "PageAccessToken": "<Key Vault>" },
    "LinkedInLeadForms": { "ClientSecret": "<Key Vault>", "AccessToken": "<Key Vault>" }
  }
}
  • Ads:DefaultPhoneCountryCode is required whenever AddWeborityAds is called.
  • Each platform section registers only when present. A present section with a missing value, or with an empty ConversionActions, stops the host at startup.
  • Every head that queues conversions needs the platform sections too. Registration is gated on configuration, so a head without Ads:Google registers no Google client and queues no Google rows.
  • ConversionActions maps a conversion key (the fleet analytics event name, e.g. signup_completed, or the product's registered event name) to the platform's identifier: a conversion action id on Google, a CRM event name (lead stage) on Meta, a conversion rule id (digits) on LinkedIn.
  • LinkedIn access tokens last 60 days and are not renewed by the package. LinkedIn rotates the refresh token on every use, so a refresh token cannot live in configuration. The product renews the token (the app's OAuth flow) and updates Ads:LinkedIn:AccessToken and Ads:LinkedInLeadForms:AccessToken before they lapse. ApiVersion (the LinkedIn-Version header) defaults to 202606 on both sections.
  • Ads:LinkedInLeadForms:ClientSecret is the LinkedIn app's client secret: it signs every delivery and answers the webhook challenge.

Queuing a conversion

await _adConversions.EnqueueAsync(new AdConversionRequest
{
    ConversionKey = AnalyticsEvents.SignupCompleted,
    SubjectReference = account.PublicId,
    OccurredDateTimeUtc = now,
    Email = user.Email,
    PhoneNumber = user.PhoneNumber,
    Touch = account.Attribution?.LastTouch,   // click ids and click time come from the touch
    MetaLeadgenId = account.MetaLeadgenId,    // when the account came from a Meta lead form
    EventId = pixelEventId                    // when the browser pixel sent the same event
}, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);   // the rows commit with the business change

One row per configured platform. A platform with no conversion action for the key, or nothing to match on, or a conversion it would refuse as too old, gets a Skipped row with the reason, so a missing report is visible rather than silent. A repeat of the same (key, subject) adds nothing.

Sending

Host AdConversionDispatcher.RunAsync on a timer in the product's Function or worker. Several hosts may run it at once: each run first claims up to 50 Pending rows (oldest first) with one conditional update that stamps ClaimedBy and a 5-minute ClaimedUntilDateTimeUtc, sends only the rows it claimed, stops before a send could outlive the lease, and releases what it did not reach. A crashed run's rows come back when its lease lapses. A retryable failure stays Pending for up to 5 attempts, a refusal fails at once. LastError holds the platform's refusal or a curated line; an unexpected exception goes to the log in full, never to the row.

Receiving leads

MapAdLeadWebhooks verifies each delivery (Meta: X-Hub-Signature-256 over the raw body; LinkedIn: X-LI-Signature over the raw body; Google: the echoed google_key, hashed and compared in fixed time), stores the AdLead, answers 200, and queues the lead for the background worker. LinkedIn's GET challenge is answered with the HMAC of its challengeCode, and only for a UUID code: the challenge and the delivery signature are the same HMAC under the same secret, so answering any other code would sign a body of the caller's choosing; a deletion notification is acknowledged and stores nothing. The worker fetches a Meta lead's answers from the Graph API and a LinkedIn lead's from the Lead Sync API (the response, then its form, so each answer is filed under what it asked: standard fields lifted out, other predefined fields under their predefinedField name, custom questions under their label, chosen options by name), calls IAdLeadHandler.HandleAsync, and records the outcome. A lead left Received (a restart, a failed attempt) is swept up every 10 minutes; after 5 failed attempts it is Failed, and AdLeadIntake.ReprocessAsync retries it on an operator's word. Google test deliveries are answered and never stored.

Each attempt claims the lead first with the same conditional-update lease (10 minutes), so two processors on one host or two never both call the handler for one attempt. A heartbeat renews the lease every third of it while the handler runs, so a handler slower than 10 minutes keeps its claim and is not started again by another worker's sweep; a renewal that finds the claim already gone (a crash right before it) logs a warning and stops, since the handler is at least once regardless. A handler that throws leaves a curated LastError (the exception type, never its text); the exception itself is logged in full. The handler is still at least once across a crash between its return and the lead being marked handled: key what it creates on AdLead.Id.

Gates

sh scripts/install-hooks.sh                        # once per clone
dotnet build Webority.Ads.slnx -v q --nologo -m:4
dotnet test Webority.Ads.slnx --no-build --nologo -v q

The pre-push hook runs both; CI only packs and publishes on merge to main.

Release status

Published on nuget.org: 0.1.0 and 0.1.1. Directory.Build.props VersionPrefix holds 0.1.1, the current released version.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Webority.Ads:

Package Downloads
Webority.Ads.Google

Google Ads for Webority.Ads: offline click conversions through the Data Manager API with a service-account token, and parsing plus key verification for Google Ads lead form webhooks.

Webority.Ads.AspNetCore

ASP.NET Core lead-form webhooks for Webority.Ads: verified Meta Lead Ads, Google Ads lead form and LinkedIn Lead Gen Form endpoints that record each lead and hand it to the product's IAdLeadHandler off the request.

Webority.Ads.Meta

Meta (Facebook and Instagram) for Webority.Ads: Conversions API for CRM events, Lead Ads lead retrieval from the Graph API, and verification of Meta webhook deliveries.

Webority.Ads.Testing

Test doubles for Webority.Ads: a scriptable conversion client, a lead details source and a recording lead handler, plus lead webhook and Graph API payload fixtures and a Meta webhook signer. No test-framework dependency.

Webority.Ads.LinkedIn

LinkedIn for Webority.Ads: Conversions API events, Lead Gen Form lead retrieval through the Lead Sync API, and verification of LinkedIn lead webhook deliveries and challenges.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.2 134 9/27/2026
0.1.1 114 9/26/2026
0.1.0 121 9/26/2026