Awin.Affiliate
1.4.0
dotnet add package Awin.Affiliate --version 1.4.0
NuGet\Install-Package Awin.Affiliate -Version 1.4.0
<PackageReference Include="Awin.Affiliate" Version="1.4.0" />
<PackageVersion Include="Awin.Affiliate" Version="1.4.0" />
<PackageReference Include="Awin.Affiliate" />
paket add Awin.Affiliate --version 1.4.0
#r "nuget: Awin.Affiliate, 1.4.0"
#:package Awin.Affiliate@1.4.0
#addin nuget:?package=Awin.Affiliate&version=1.4.0
#tool nuget:?package=Awin.Affiliate&version=1.4.0
Awin.Affiliate
A lightweight, strongly typed .NET SDK for the Awin Publisher API — affiliate link generation and reports.
Small .NET SDK for the Awin Publisher API.
Maintained by GrecoLabs.
Awin.Affiliate helps affiliate bots, deal monitors, and revenue dashboards build Awin tracking deep links and pull reports (transactions, sales summary, click stats) from the Awin Publisher API.
What It Does
| Capability | Description |
|---|---|
| Affiliate links | Builds Awin tracking deep links (cread.php?awinmid=...&awinaffid=...&ued=...) entirely client-side. |
| Sub-id tagging | Appends up to two click references (clickref, clickref2) per Awin's spec. |
| Short URL resolving | Optionally follows redirects on the input URL before building the affiliate link. |
| Transactions | Lists conversions from GET /publishers/{publisherId}/transactions/. |
| Sales summary | Aggregates a period's transactions into gross revenue, commission, conversion counts, and a top-advertiser breakdown. |
| Click stats | Reads click/impression totals from the Awin advertiser performance report — aggregated and broken down per advertiser. |
| Generated link usage | Pulls per-link click/impression/conversion stats from the creative report (registered creatives only). |
| Advertisers | Lists every programme the publisher has access to via GET /publishers/{publisherId}/programmes/. |
| Credential validation | Explicitly validates the configured Publisher API credentials and surfaces stable credential failure signals. |
Installation
After the package is published to NuGet:
dotnet add package Awin.Affiliate
To build from source:
git clone https://github.com/gregojoao/awin-affiliate.git
cd awin-affiliate
dotnet restore
dotnet test
dotnet pack -c Release
The package is generated at:
src/Awin.Affiliate/bin/Release/Awin.Affiliate.<version>.nupkg
Quick Start
using Awin.Affiliate.Application;
var options = new AwinAffiliateOptions
{
PublisherId = Environment.GetEnvironmentVariable("AWIN_PUBLISHER_ID")!
};
using var httpClient = new HttpClient();
var client = new AwinAffiliateClient(httpClient, options);
var result = await client.GenerateAffiliateLinkAsync(new AwinAffiliateLinkRequest
{
OriginUrl = new Uri("https://www.kabum.com.br/produto/123"),
AdvertiserId = "12345", // Awin advertiser id (a.k.a. awinmid)
SubIds = new[] { "telegram", "promo-summer" } // clickref / clickref2
});
Console.WriteLine(result.AffiliateUrl);
Console.WriteLine(result.Source); // TrackingDeepLink
Console.WriteLine(result.OriginUrl);
Configuration
You can pass credentials manually:
using Awin.Affiliate.Application;
var options = new AwinAffiliateOptions
{
PublisherId = "your-publisher-id",
Endpoint = AwinAffiliateOptions.DefaultEndpoint,
TrackingEndpoint = AwinAffiliateOptions.DefaultTrackingEndpoint,
Timeout = TimeSpan.FromSeconds(30)
};
For ASP.NET Core, Worker Services, or any app using Microsoft.Extensions.DependencyInjection, register the SDK once:
using Awin.Affiliate.Infrastructure;
builder.Services.AddAwinAffiliate(builder.Configuration);
Then configure secrets through environment variables, user secrets, Key Vault, or any other configuration provider:
{
"Awin": {
"Affiliate": {
"PublisherId": "your-publisher-id",
"Timeout": "00:00:30"
}
}
}
In production, prefer environment variables or a secret manager instead of committing secrets to appsettings.json.
After registration, inject the client:
using Awin.Affiliate.Application;
public sealed class DealPublisher(IAwinAffiliateClient awin)
{
public async Task PublishAsync(string productUrl, string advertiserId)
{
var result = await awin.GenerateAffiliateLinkAsync(new AwinAffiliateLinkRequest
{
OriginUrl = new Uri(productUrl),
AdvertiserId = advertiserId
});
Console.WriteLine(result.AffiliateUrl);
}
}
You can also configure options directly in code:
using Awin.Affiliate.Infrastructure;
builder.Services.AddAwinAffiliate(options =>
{
options.PublisherId = builder.Configuration["AWIN_PUBLISHER_ID"]!;
});
| Option | Default | Purpose |
|---|---|---|
Endpoint |
https://api.awin.com |
Awin Publisher API endpoint (used by short-URL resolution). |
TrackingEndpoint |
https://www.awin1.com/cread.php |
cread.php tracking endpoint used to build deep links. |
PublisherId |
Empty | Awin publisher id (your awinaffid). Required. |
AccessToken |
Empty | OAuth2 long-lived access token. Only required for Reports calls. |
Timeout |
00:00:30 |
HTTP request timeout. |
Per-call behavior lives on request objects:
| Request Property | Default | Purpose |
|---|---|---|
AwinAffiliateLinkRequest.OriginUrl |
required | Destination URL the affiliate link will redirect to. |
AwinAffiliateLinkRequest.AdvertiserId |
required | Awin advertiser id (awinmid). Without it the link does not track. |
AwinAffiliateLinkRequest.SubIds |
Empty | Optional tracking sub-ids (mapped to clickref / clickref2). Awin honours up to two. |
AwinAffiliateLinkRequest.ResolveShortUrls |
false |
Follows redirects on the origin URL before building the affiliate link. |
Main APIs
Use IAwinAffiliateClient when credentials are registered through DI. Use AwinAffiliateClient directly when you want to provide AwinAffiliateOptions in code.
GenerateAffiliateLinkAsync
Builds an Awin tracking deep link. No HTTP call is made unless ResolveShortUrls = true.
var result = await client.GenerateAffiliateLinkAsync(new AwinAffiliateLinkRequest
{
OriginUrl = new Uri(productUrl),
AdvertiserId = "12345",
SubIds = new[] { "telegram" }
});
// result.AffiliateUrl =
// https://www.awin1.com/cread.php?awinmid=12345&awinaffid=987654&ued=<encoded>&clickref=telegram
ResolveAwinUrlAsync
Follows redirects for a short URL and returns the final destination. Returns the input URL unchanged on network failure.
Uri resolved = await client.ResolveAwinUrlAsync(new Uri(shortUrl));
Affiliate reports
Reports live in Awin.Affiliate.Reports.Application. They have a separate options type and DI extension so callers that only need link generation don't have to provide an access token.
using Awin.Affiliate.Reports.Configuration;
using Awin.Affiliate.Reports.Application;
var options = new AwinAffiliateReportsOptions
{
PublisherId = Environment.GetEnvironmentVariable("AWIN_PUBLISHER_ID")!,
AccessToken = Environment.GetEnvironmentVariable("AWIN_ACCESS_TOKEN")!,
};
using var httpClient = new HttpClient();
IAwinAffiliateReportsClient reports = new AwinAffiliateReportsClient(httpClient, options);
Validate credentials explicitly before running scheduled jobs, onboarding a publisher account, or wiring integrations that need a stable "credentials rejected" signal:
using Awin.Affiliate.Infrastructure;
try
{
await reports.ValidateCredentialsAsync();
}
catch (AwinAffiliateAuthException ex) when (ex.IsCredentialError)
{
Console.WriteLine($"{ex.Platform}: {ex.Kind}");
Console.WriteLine(ex.ProviderErrorCode);
Console.WriteLine(ex.ProviderMessage);
}
Or with DI:
using Awin.Affiliate.Reports.DependencyInjection;
builder.Services.AddAwinAffiliateReports(builder.Configuration);
Reports configuration section:
{
"Awin": {
"Reports": {
"PublisherId": "987654",
"AccessToken": "your-oauth2-token",
"DateType": "transaction",
"Timezone": "Europe/London"
}
}
}
ListConversionsAsync
using Awin.Affiliate.Reports.Application.Requests;
var page = await reports.ListConversionsAsync(new ListAwinConversionsRequest
{
PeriodStart = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
PeriodEnd = DateOnly.FromDateTime(DateTime.UtcNow),
Status = AwinConversionStatusFilter.Approved,
AdvertiserIds = new[] { "12345" } // optional
});
foreach (var tx in page.Items)
{
Console.WriteLine($"{tx.AdvertiserName} — {tx.CommissionAmount} ({tx.Status})");
}
ValidateCredentialsAsync
Calls the lightweight programmes endpoint (GET /publishers/{publisherId}/programmes/) to verify that the configured PublisherId and AccessToken are accepted by Awin.
await reports.ValidateCredentialsAsync();
On success, the method returns normally. On HTTP 401/403, it propagates AwinAffiliateAuthException with a stable credential signal:
| Property | Value |
|---|---|
Platform |
"awin" |
Kind |
Unauthorized for HTTP 401, Forbidden for HTTP 403 |
ProviderErrorCode |
Error code extracted from Awin's JSON body, when present. |
ProviderMessage |
Error message extracted from Awin's JSON body, when present. |
IsCredentialError |
true |
IsRetryable |
false |
HTTP 429 still throws AwinAffiliateRateLimitException; 5xx, network, and timeout failures stay under AwinAffiliateApiException rather than being treated as credential failures.
GetSalesSummaryAsync
var summary = await reports.GetSalesSummaryAsync(new AwinSalesSummaryRequest
{
PeriodStart = new DateOnly(2026, 5, 1),
PeriodEnd = new DateOnly(2026, 5, 31)
});
Console.WriteLine($"Conversions: {summary.Conversions}");
Console.WriteLine($"Gross: {summary.GrossRevenue}");
Console.WriteLine($"Commission: {summary.Commission}");
Console.WriteLine($"Avg rate: {summary.AvgCommissionRate:F2}%");
foreach (var advertiser in summary.TopAdvertisers)
{
Console.WriteLine($" {advertiser.AdvertiserName}: {advertiser.Commission}");
}
AwinSalesSummary carries:
| Property | Description |
|---|---|
PeriodStart / PeriodEnd |
Reporting window. |
Conversions |
Number of conversions. |
Clicks |
Total clicks, when available. |
GrossRevenue |
Sum of sale amounts. |
Commission |
Sum of commissionable amounts. |
AvgCommissionRate |
commission / gross * 100. Zero when gross is zero. |
ConversionRate |
conversions / clicks (when clicks are available). |
ByStatus |
Conversion counts grouped by status. |
TopAdvertisers |
Per-advertiser commission breakdown. |
Supported |
false when the SDK had to degrade (e.g. no access to the report endpoint). |
UnsupportedReason |
Why the result is degraded. |
GetConversionAsync
Searches the last 31 days for a transaction matching the given order reference; throws AwinAffiliateNotFoundException when nothing matches.
var tx = await reports.GetConversionAsync("ORDER-42");
ListAdvertisersAsync
Returns every programme (advertiser) accessible to the configured publisher.
var advertisers = await reports.ListAdvertisersAsync();
GetClickStatsAsync and GetGeneratedLinkUsageAsync
GetClickStatsAsync reads the advertiser performance report (/publishers/{id}/reports/advertiser/); GetGeneratedLinkUsageAsync reads the creative report (/publishers/{id}/reports/creative/), which is scoped to registered creatives. When the account lacks access, or Awin rejects the input, the SDK returns Supported = false with a populated UnsupportedReason instead of throwing.
Why not the creative report for clicks? It only counts registered creatives. Publishers that build deep links on the fly (
cread.php?ued=…) get200 []there, while the advertiser report returns their traffic. Verified against a live BR account:creative→[],advertiser→{"advertiserId":17729,"clicks":41}for the same window.
Region is required by both reports — omitting it answers HTTP 400 invalid region code list. It has no default (a hardcoded one would answer 200 [] for every publisher outside it — zero clicks that look like a valid answer), and the report calls throw InvalidOperationException when it is empty. It accepts a comma-separated list (GB,IE). Note also that DateType only accepts transaction or validation: Awin rejects click.
A region that is merely wrong (say
GBon a BR account) still answers200 []. If you need to tell "no traffic" from "wrong region", cross-check withListAdvertisersAsync: programmes present while the advertiser report comes back empty means the region is off.
Clicks come back both aggregated and per advertiser, since the report returns one row per store:
stats.Clicks; // 50 — total across advertisers
stats.PerAdvertiser[0].AdvertiserId; // "17729"
stats.PerAdvertiser[0].AdvertiserName; // "Kabum BR"
stats.PerAdvertiser[0].Clicks; // 41
Clicks may also arrive on rows with no advertiser. Those are counted in Clicks but absent from PerAdvertiser, so the invariant is Clicks == sum(PerAdvertiser.Clicks) + UnattributedClicks. A non-zero UnattributedClicks means the report shape changed and per-advertiser mirroring is losing rows.
Neither report supports a daily breakdown: the response aggregates the whole window. To get a value per day, call it once per day (PeriodStart == PeriodEnd) — the daily figures sum exactly to the aggregate.
⚠️ The day is cut in
Timezone, which defaults toEurope/London. Going day by day without matching your own day boundary silently reshuffles the series: the window total stays right and the individual days move. Measured on one real account, same week, same total of 41 clicks:UTC 4 2 9 7 9 9 1 Europe/London 4 1 9 6 11 9 1 America/Sao_Paulo 3 4 8 6 12 6 2
FormatStart/FormatEndbuildT00:00:00/T23:59:59that Awin interprets in that timezone, so theDateOnlyyou compute and theTimezoneyou pass must come from the same clock — otherwise you store an off-by-one day.
var clicks = await reports.GetClickStatsAsync(new AwinClickStatsRequest
{
PeriodStart = new DateOnly(2026, 5, 1),
PeriodEnd = new DateOnly(2026, 5, 31)
});
var usage = await reports.GetGeneratedLinkUsageAsync(new AwinGeneratedLinkUsageRequest
{
PeriodStart = new DateOnly(2026, 5, 1),
PeriodEnd = new DateOnly(2026, 5, 31),
LinkId = "creative-id-or-link-id"
});
Methods and exceptions
| Method | Awin endpoint | May throw |
|---|---|---|
GenerateAffiliateLinkAsync |
none (client-side build) | ArgumentException on missing AdvertiserId / invalid URL, InvalidOperationException on missing options. |
ResolveAwinUrlAsync |
(HTTP GET on origin URL) | AwinAffiliateApiException on timeout. Returns the input URL on other failures. |
ValidateCredentialsAsync |
GET /publishers/{publisherId}/programmes/ |
AwinAffiliateAuthException (401/403), AwinAffiliateRateLimitException (429), AwinAffiliateApiException (4xx/5xx/network/timeout). |
ListConversionsAsync |
GET /publishers/{publisherId}/transactions/ |
AwinAffiliateAuthException (401/403), AwinAffiliateRateLimitException (429), AwinAffiliateApiException (4xx/5xx). |
GetConversionAsync |
GET /publishers/{publisherId}/transactions/ (filtered locally) |
AwinAffiliateNotFoundException when nothing matches. Plus the same HTTP exceptions as above. |
GetSalesSummaryAsync |
GET /publishers/{publisherId}/transactions/ |
Same as ListConversionsAsync. |
GetClickStatsAsync |
GET /publishers/{publisherId}/reports/advertiser/ |
Degrades to Supported = false on 400/401/403/404. Requires region. |
GetGeneratedLinkUsageAsync |
GET /publishers/{publisherId}/reports/creative/ |
Degrades to Supported = false on 400/401/403/404. Requires region. Counts only REGISTERED creatives — deep-link publishers get zeros. |
ListAdvertisersAsync |
GET /publishers/{publisherId}/programmes/ |
Standard HTTP exceptions. |
Exception hierarchy:
AwinAffiliateException
├── AwinAffiliateApiException (4xx/5xx, malformed body)
├── AwinAffiliateAuthException (401/403)
├── AwinAffiliateRateLimitException (429)
├── AwinAffiliateNotFoundException (missing resource)
└── AwinAffiliateUnsupportedException (feature not available)
AwinAffiliateAuthException is the programmatic credential failure signal for integrations such as Awin-backed merchant adapters:
| Property | Description |
|---|---|
Platform |
Stable platform id: "awin". |
Kind |
AwinAffiliateCredentialFailureKind (Invalid, Expired, Unauthorized, Forbidden). HTTP 401 maps to Unauthorized; HTTP 403 maps to Forbidden. |
ProviderErrorCode |
Provider code extracted from JSON fields like error, code, or errorCode, when available. |
ProviderMessage |
Provider message extracted from JSON fields like message, error_description, errorMessage, detail, or title, when available. |
IsCredentialError |
Always true. |
IsRetryable |
Always false. |
The transport sanitizes authentication response bodies before attaching them to the exception, including token-like fields and bearer-token text.
Limits and conventions
- 31-day window: Awin's
transactions/andreports/aggregated/endpoints reject windows larger than 31 days. The SDK validates this client-side and throwsArgumentExceptionearly. - Time zone: defaults to
Europe/London(Awin's account default). Override viaAwinAffiliateReportsOptions.Timezone. - Token lifetime: OAuth2 tokens issued via
Toolbox > API credentialsare long-lived and do not need refresh. The SDK simply sends them asAuthorization: Bearer {token}. - Rate limit: Awin documents a soft limit around 20 requests/second per publisher. The transport retries once on transient 5xx and timeout errors with exponential backoff; 429 responses surface as
AwinAffiliateRateLimitException(no implicit retry — back off and retry yourself). - Sub-ids: Awin honours
clickrefandclickref2. Sub-ids beyond the first two are silently dropped. - Currency: every monetary value flows through the
Moneyvalue object ({ Amount, Currency }). Aggregations refuse to combine mismatched currencies.
Authentication
- Sign in to https://ui.awin.com.
- Open Toolbox > API credentials.
- Click Create OAuth2 token.
- Store the token in a secret manager and expose it as
AwinAffiliateReportsOptions.AccessToken.
The link client (AwinAffiliateClient) does not need a token — Awin tracking deep links are built client-side.
GenerateAffiliateLinkAsync intentionally does not validate credentials over the network. If your application needs to confirm that an Awin AccessToken is valid, call IAwinAffiliateReportsClient.ValidateCredentialsAsync explicitly.
Architecture
The SDK is organized with a small DDD-inspired structure:
| Layer | Responsibility |
|---|---|
Domain |
Value objects (Money, AwinAdvertiserIdentity, AwinPublisherIdentity, AwinTransactionStatus). |
Application |
Public use cases and service abstractions (IAwinAffiliateClient, request/result records, options). |
Infrastructure |
HTTP transport, link builder, response mappers, exception hierarchy, DI registration. |
Reports |
Reports-specific surface (IAwinAffiliateReportsClient, requests, options, transport, mappers) — kept in a sub-namespace so callers that only need link generation can ignore it. |
Public namespaces follow the physical project structure:
Awin.Affiliate.Application— link client, options, requests, resultsAwin.Affiliate.Domain— value objectsAwin.Affiliate.Infrastructure— DI registration, exceptions, defaultsAwin.Affiliate.Reports.Application— reports client, requestsAwin.Affiliate.Reports.Configuration— reports optionsAwin.Affiliate.Reports.Domain— typed reports modelsAwin.Affiliate.Reports.DependencyInjection—AddAwinAffiliateReports
Returned Data
AwinAffiliateLinkResult contains:
| Property | Description |
|---|---|
AffiliateUrl |
Affiliate URL ready to share. |
OriginUrl |
Destination URL (after redirect resolution, when requested). |
AdvertiserId |
Advertiser id used to build the link. |
Source |
TrackingDeepLink (always for this release; ApiConverted reserved). |
AwinConversion contains:
| Property | Description |
|---|---|
Id |
Awin transaction id. |
AdvertiserId / AdvertiserName |
Programme that drove the conversion. |
SaleAmount |
Sale gross amount. |
CommissionAmount |
Commission payable. |
Status |
Typed AwinTransactionStatus (Approved / Pending / Declined / Unknown). |
TransactionDate |
When the sale (or click) was recorded. |
OrderReference |
Advertiser-supplied order reference (orderRef). |
ClickRef / ClickRef2 |
Sub-ids recorded by Awin (your clickref / clickref2). |
Supported URL formats
The SDK accepts any HTTP/HTTPS URL as the affiliate destination. With ResolveShortUrls = true, the SDK first follows redirects on the input — handy for short URLs such as https://s.click.partner.test/abc.
var resolved = await client.ResolveAwinUrlAsync(new Uri("https://s.click.test/abc"));
The cread.php link itself works on every Awin endpoint that uses the awin1.com tracking infrastructure (www.awin1.com, awin.com, regional aliases).
Development
dotnet restore
dotnet test
dotnet pack -c Release
Publishing
Before publishing a new NuGet version:
- Update
<Version>and<PackageReleaseNotes>insrc/Awin.Affiliate/Awin.Affiliate.csproj. - Run the validation:
dotnet test
dotnet pack -c Release
- Push the package:
dotnet nuget push src/Awin.Affiliate/bin/Release/Awin.Affiliate.*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
See PUBLISHING.md for the full release checklist.
License
This project is licensed under the MIT License. See LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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. |
-
net10.0
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
-
net8.0
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.4.0 (behaviour change, read before upgrading): GetClickStatsAsync now reads the ADVERTISER performance report (/reports/advertiser/) instead of the creative report (/reports/creative/) — the creative report only counts registered creatives and answers 200 [] for publishers that build deep links on the fly, so the numbers this call returns will change. Both report calls now require the new Region option (no default; they throw when it is empty) because Awin answers HTTP 400 'invalid region code list' without it. GetGeneratedLinkUsageAsync now sends region too (it was failing on every call) and degrades on 400 like GetClickStatsAsync. AwinClickStats gains PerAdvertiser (per-advertiser breakdown) and UnattributedClicks (rows without an advertiser id), with the invariant Clicks == sum(PerAdvertiser) + UnattributedClicks. Note: DateType only accepts 'transaction' or 'validation' — 'click' was never valid and the XML doc claiming otherwise is fixed. 1.2.0: Adds stable credential failure signals for Awin authentication failures and explicit reports credential validation. 1.1.0: multi-targeting for .NET 8 and .NET 10. Tracking deep-link generation and reports surface (transactions, sales summary, click stats, generated link usage, advertisers) built on the Awin Publisher API.