Awin.Affiliate 1.4.0

dotnet add package Awin.Affiliate --version 1.4.0
                    
NuGet\Install-Package Awin.Affiliate -Version 1.4.0
                    
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="Awin.Affiliate" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Awin.Affiliate" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Awin.Affiliate" />
                    
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 Awin.Affiliate --version 1.4.0
                    
#r "nuget: Awin.Affiliate, 1.4.0"
                    
#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 Awin.Affiliate@1.4.0
                    
#: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=Awin.Affiliate&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Awin.Affiliate&version=1.4.0
                    
Install as a Cake Tool

Awin.Affiliate

A lightweight, strongly typed .NET SDK for the Awin Publisher API — affiliate link generation and reports.

CI NuGet License: MIT .NET

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=…) get 200 [] 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 GB on a BR account) still answers 200 []. If you need to tell "no traffic" from "wrong region", cross-check with ListAdvertisersAsync: 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 to Europe/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/FormatEnd build T00:00:00/T23:59:59 that Awin interprets in that timezone, so the DateOnly you compute and the Timezone you 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/ and reports/aggregated/ endpoints reject windows larger than 31 days. The SDK validates this client-side and throws ArgumentException early.
  • Time zone: defaults to Europe/London (Awin's account default). Override via AwinAffiliateReportsOptions.Timezone.
  • Token lifetime: OAuth2 tokens issued via Toolbox > API credentials are long-lived and do not need refresh. The SDK simply sends them as Authorization: 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 clickref and clickref2. Sub-ids beyond the first two are silently dropped.
  • Currency: every monetary value flows through the Money value object ({ Amount, Currency }). Aggregations refuse to combine mismatched currencies.

Authentication

  1. Sign in to https://ui.awin.com.
  2. Open Toolbox > API credentials.
  3. Click Create OAuth2 token.
  4. 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, results
  • Awin.Affiliate.Domain — value objects
  • Awin.Affiliate.Infrastructure — DI registration, exceptions, defaults
  • Awin.Affiliate.Reports.Application — reports client, requests
  • Awin.Affiliate.Reports.Configuration — reports options
  • Awin.Affiliate.Reports.Domain — typed reports models
  • Awin.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:

  1. Update <Version> and <PackageReleaseNotes> in src/Awin.Affiliate/Awin.Affiliate.csproj.
  2. Run the validation:
dotnet test
dotnet pack -c Release
  1. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.0 423 8/30/2026
1.3.0 611 7/16/2026
1.2.1 690 5/22/2026
1.2.0 837 5/21/2026
1.1.0 165 5/19/2026

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.