### Install NuGet Package Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Use the .NET CLI to add the App Store Server Library package to your project. Ensure you have .NET 8.0 or later installed. ```bash dotnet add package Enjna.AppStoreServerLibrary ``` -------------------------------- ### Get Refund History Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Paginates through the refund history for a customer using a revision token. Collects all refunded signed transactions. Continues fetching until no more refunds are available. ```csharp RefundHistoryResponse? refundResponse = null; var refundedSignedTransactions = new List(); do { refundResponse = await client.GetRefundHistoryAsync( anyTransactionId: "1000000123456789", bundleId: "com.example.myapp", revision: refundResponse?.Revision ); if (refundResponse.SignedTransactions is not null) refundedSignedTransactions.AddRange(refundResponse.SignedTransactions); } while (refundResponse.HasMore); Console.WriteLine($"Customer has {refundedSignedTransactions.Count} refunded transaction(s)"); ``` -------------------------------- ### Retrieve Transaction History Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Fetch a customer's transaction history using the App Store Server API. This example demonstrates paginating through results and filtering by product type. Requires an initialized AppStoreServerAPIClient. ```csharp var issuerId = "99b16628-15e4-4668-972b-eeff55eeff55"; var keyId = "ABCDEFGHIJ"; var bundleId = "com.example"; var privateKey = File.ReadAllText("/path/to/key.p8"); var environment = AppStoreEnvironment.Sandbox; var client = new AppStoreServerAPIClient(privateKey, keyId, issuerId, environment); var appReceipt = "MI..."; var receiptUtility = new ReceiptUtility(); var transactionId = receiptUtility.ExtractTransactionIdFromAppReceipt(appReceipt); if (transactionId is not null) { var request = new TransactionHistoryRequest { Sort = SortOrder.Ascending, Revoked = false, ProductTypes = [ProductType.AutoRenewable] }; HistoryResponse? response = null; var transactions = new List(); do { request.Revision = response?.Revision; response = await client.GetTransactionHistoryAsync(transactionId, bundleId, request); if (response.SignedTransactions is not null) { transactions.AddRange(response.SignedTransactions); } } while (response.HasMore); Console.WriteLine($"Found {transactions.Count} transactions"); } ``` -------------------------------- ### Request and Get Test Notification Status Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Trigger a test App Store Server Notification and then poll for its delivery status. The `TestNotificationToken` is required for polling. ```csharp // Trigger a test notification var testResponse = await client.RequestTestNotificationAsync("com.example.myapp"); Console.WriteLine($ ``` -------------------------------- ### Get All Subscription Statuses Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Retrieves all auto-renewable subscription statuses for a customer. Optionally filter by specific status values like active or expired. Handles potential 'AccountNotFound' API exceptions. ```csharp using Enjna.AppStoreServerLibrary.Models.Enums; try { var statusResponse = await client.GetAllSubscriptionStatusesAsync( anyTransactionId: "1000000123456789", bundleId: "com.example.myapp", status: [Status.Active, Status.BillingRetry] ); foreach (var group in statusResponse.Data ?? []) { Console.WriteLine($"Subscription group: {group.SubscriptionGroupIdentifier}"); foreach (var item in group.LastTransactions ?? []) { Console.WriteLine($" originalTxId={item.OriginalTransactionId}, status={item.Status}"); // item.SignedTransactionInfo and item.SignedRenewalInfo are JWS strings for verification } } } catch (APIException ex) when (ex.ApiError == APIError.AccountNotFound) { Console.Error.WriteLine("Customer account not found in App Store"); } ``` -------------------------------- ### SignedDataVerifier.VerifyAndDecodeRealtimeRequestAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Decodes the signed payload sent by Apple to your `GET /retention-message` endpoint. This is used for handling real-time retention messaging requests from Apple. ```APIDOC ## SignedDataVerifier.VerifyAndDecodeRealtimeRequestAsync — Decode a Retention Messaging real-time request ### Description Decodes the signed payload sent by Apple to your `GET /retention-message` endpoint. ### Method `VerifyAndDecodeRealtimeRequestAsync` ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None ### Request Example ```csharp // In your HTTP handler for Apple's real-time retention messaging request: var requestBody = await verifier.VerifyAndDecodeRealtimeRequestAsync( signedPayload: signedPayloadFromApple, appAppleId: 1234567890L ); ``` ### Response #### Success Response Returns a `RealtimeRequest` object containing decoded retention request data. #### Response Example ```csharp Console.WriteLine($"originalTransactionId: {requestBody.OriginalTransactionId}"); Console.WriteLine($"productId : {requestBody.ProductId}"); // Respond with a retention message (e.g. a promotional offer) var realtimeResponse = new RealtimeResponseBody { PromotionalOffer = new PromotionalOffer { OfferId = "my-winback-offer", Signature = promotionalOfferSignature } }; // Return realtimeResponse serialized as JSON with HTTP 200 ``` ``` -------------------------------- ### Get Transaction Info Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Fetches the signed transaction payload for a specific transaction identifier. The 'SignedTransactionInfo' property is a JWS string that needs verification using 'SignedDataVerifier'. Handles general API exceptions. ```csharp try { var info = await client.GetTransactionInfoAsync( transactionId: "1000000123456789", bundleId: "com.example.myapp" ); // info.SignedTransactionInfo is a JWS string — verify with SignedDataVerifier Console.WriteLine(info.SignedTransactionInfo); } catch (APIException ex) { Console.Error.WriteLine($"Error: {ex.ApiError} — {ex.ErrorMessage}"); } ``` -------------------------------- ### Build and Test Commands for .NET Solution Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/CLAUDE.md Standard commands for building, testing, and running the .NET solution. Use the filter-method option for targeted test execution. ```bash dotnet build # Build the solution ``` ```bash dotnet test # Run all tests ``` ```bash dotnet run --project test/Enjna.AppStoreServerLibrary.Tests # Run tests directly (verbose output) ``` ```bash dotnet run --project test/Enjna.AppStoreServerLibrary.Tests -- --filter-method "*MethodName*" ``` -------------------------------- ### Initialize App Store Server API Client Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Instantiate the AppStoreServerAPIClient with your credentials and environment. This client is used to interact with the App Store Server API. ```csharp var issuerId = "99b16628-15e4-4668-972b-eeff55eeff55"; var keyId = "ABCDEFGHIJ"; var bundleId = "com.example"; var privateKey = File.ReadAllText("/path/to/key.p8"); var environment = AppStoreEnvironment.Sandbox; var client = new AppStoreServerAPIClient(privateKey, keyId, issuerId, environment); var response = await client.RequestTestNotificationAsync(bundleId); Console.WriteLine(response.TestNotificationToken); ``` -------------------------------- ### AppStoreServerAPIClient Constructor Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Demonstrates how to create an authenticated HTTP client for the App Store Server API. It shows basic construction and integration with ASP.NET Core Dependency Injection using IHttpClientFactory. ```APIDOC ## AppStoreServerAPIClient Constructor Creates an authenticated HTTP client for the App Store Server API. Pass an optional pre-existing `HttpClient` to integrate with `IHttpClientFactory` and avoid socket exhaustion. ```csharp using Enjna.AppStoreServerLibrary; using Enjna.AppStoreServerLibrary.Models.Enums; // Basic construction — the library manages its own HttpClient var client = new AppStoreServerAPIClient( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55", environment: AppStoreEnvironment.Sandbox ); // ASP.NET Core DI — use IHttpClientFactory to avoid socket exhaustion builder.Services.AddHttpClient("AppStoreServer"); builder.Services.AddSingleton(sp => { var httpClient = sp.GetRequiredService().CreateClient("AppStoreServer"); return new AppStoreServerAPIClient( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55", environment: AppStoreEnvironment.Production, httpClient: httpClient // client will NOT be disposed by the library ); }); ``` ``` -------------------------------- ### Decode Real-time Retention Messaging Request Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Decodes the signed payload sent by Apple to your GET /retention-message endpoint. This is used for handling real-time retention messaging requests from Apple. ```csharp // In your HTTP handler for Apple's real-time retention messaging request: var requestBody = await verifier.VerifyAndDecodeRealtimeRequestAsync( signedPayload: signedPayloadFromApple, appAppleId: 1234567890L ); Console.WriteLine($"originalTransactionId: {requestBody.OriginalTransactionId}"); Console.WriteLine($"productId : {requestBody.ProductId}"); Console.WriteLine($"userLocale : {requestBody.UserLocale}"); Console.WriteLine($"requestIdentifier : {requestBody.RequestIdentifier}"); // Respond with a retention message (e.g. a promotional offer) var realtimeResponse = new RealtimeResponseBody { PromotionalOffer = new PromotionalOffer { OfferId = "my-winback-offer", Signature = promotionalOfferSignature } }; // Return realtimeResponse serialized as JSON with HTTP 200 ``` -------------------------------- ### Create Introductory Offer Eligibility JWS Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Creates a JWS that overrides Apple's default eligibility determination for introductory offers. Requires a private key, key ID, and issuer ID. ```csharp using Enjna.AppStoreServerLibrary; var eligibilityCreator = new IntroductoryOfferEligibilitySignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55" ); var jws = eligibilityCreator.CreateSignature( productId: "com.example.myapp.subscription.monthly", allowIntroductoryOffer: true, transactionId: "1000000123456789", bundleId: "com.example.myapp" ); Console.WriteLine($"Eligibility JWS: {jws}"); ``` -------------------------------- ### Register PromotionalOfferSignatureCreator as Singleton Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Register the PromotionalOfferSignatureCreator as a singleton. This service is stateless and can be safely registered as a singleton. ```csharp builder.Services.AddSingleton(new PromotionalOfferSignatureCreator( signingKey: File.ReadAllText("/path/to/key.p8"), keyId: "ABCDEFGHIJ" )); ``` -------------------------------- ### Create Promotional Offer Signature (Original StoreKit API) Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Generates a Base64-encoded ECDSA-SHA256 signature for presenting a promotional subscription offer via the original StoreKit API. Requires a private key file and key ID. ```csharp using Enjna.AppStoreServerLibrary; using var signatureCreator = new PromotionalOfferSignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ" ); var nonce = Guid.NewGuid(); var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var signature = signatureCreator.CreateSignature( productIdentifier: "com.example.myapp.subscription.annual", subscriptionOfferId: "promo-50off", appAccountToken: "6a72b3fd-4b7c-4e60-9e4d-b9a1c2d3e4f5", nonce: nonce, timestamp: timestamp, bundleId: "com.example.myapp" ); // Return { keyIdentifier, nonce, timestamp, signature } to the client for StoreKit Console.WriteLine($"Signature: {signature}"); ``` -------------------------------- ### IntroductoryOfferEligibilitySignatureCreator Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Creates a JWS that overrides Apple's default eligibility determination for introductory offers. ```APIDOC ## IntroductoryOfferEligibilitySignatureCreator — Create an introductory offer eligibility JWS Creates a JWS that overrides Apple's default eligibility determination for introductory offers. ```csharp using Enjna.AppStoreServerLibrary; var eligibilityCreator = new IntroductoryOfferEligibilitySignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55" ); var jws = eligibilityCreator.CreateSignature( productId: "com.example.myapp.subscription.monthly", allowIntroductoryOffer: true, transactionId: "1000000123456789", bundleId: "com.example.myapp" ); Console.WriteLine($"Eligibility JWS: {jws}"); ``` ``` -------------------------------- ### Initialize AppStoreServerAPIClient with HttpClientFactory Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Configure dependency injection for the AppStoreServerAPIClient in ASP.NET Core using IHttpClientFactory to prevent socket exhaustion. The provided HttpClient will not be disposed by the library. ```csharp using Enjna.AppStoreServerLibrary; using Enjna.AppStoreServerLibrary.Models.Enums; // Basic construction — the library manages its own HttpClient var client = new AppStoreServerAPIClient( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55", environment: AppStoreEnvironment.Sandbox ); // ASP.NET Core DI — use IHttpClientFactory to avoid socket exhaustion builder.Services.AddHttpClient("AppStoreServer"); builder.Services.AddSingleton(sp => { var httpClient = sp.GetRequiredService().CreateClient("AppStoreServer"); return new AppStoreServerAPIClient( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55", environment: AppStoreEnvironment.Production, httpClient: httpClient // client will NOT be disposed by the library ); }); ``` -------------------------------- ### Send Consumption Information Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Report consumption data to influence refund decisions after receiving a CONSUMPTION_REQUEST notification. Ensure `CustomerConsented` is true if applicable. ```csharp using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; await client.SendConsumptionInformationAsync( transactionId: "1000000123456789", request: new ConsumptionRequest { CustomerConsented = true, ConsumptionPercentage = 75000, // 75.000% in milliunits DeliveryStatus = DeliveryStatus.DeliveredAndWorking, SampleContentProvided = false, RefundPreference = RefundPreference.NoPreference }, bundleId: "com.example.myapp" ); ``` -------------------------------- ### Register ReceiptUtility as Singleton Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Register the ReceiptUtility service as a singleton. This service does not have external dependencies that require special handling. ```csharp builder.Services.AddSingleton(); ``` -------------------------------- ### Create Promotional Offer Signature Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Generate a signature for a promotional offer using the PromotionalOfferSignatureCreator. This is used to create deep links for offers. Ensure you have your private key, key ID, and bundle ID. ```csharp var keyId = "ABCDEFGHIJ"; var bundleId = "com.example"; var privateKey = File.ReadAllText("/path/to/key.p8"); var productId = ""; var subscriptionOfferId = ""; var appAccountToken = ""; var nonce = Guid.NewGuid(); var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); using var signatureCreator = new PromotionalOfferSignatureCreator(privateKey, keyId); var signature = signatureCreator.CreateSignature(productId, subscriptionOfferId, appAccountToken, nonce, timestamp, bundleId); Console.WriteLine(signature); ``` -------------------------------- ### Register AppStoreServerAPIClient with Typed Client Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Register the AppStoreServerAPIClient as a typed client. This method simplifies registration and allows for custom configuration of the HttpClient used internally. ```csharp // Registered as transient by default builder.Services.AddHttpClient() .AddTypedClient((httpClient) => { var privateKey = File.ReadAllText("/path/to/key.p8"); return new AppStoreServerAPIClient( privateKey, keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55", environment: AppStoreEnvironment.Production, httpClient: httpClient ); }); ``` -------------------------------- ### Error Handling Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Details on how to handle API exceptions and verification exceptions thrown by the AppStoreServerAPIClient and SignedDataVerifier. ```APIDOC ## Error Handling — APIException and VerificationException All `AppStoreServerAPIClient` methods throw `APIException` on non-2xx HTTP responses. `SignedDataVerifier` methods throw `VerificationException` on cryptographic or validation failures. ```csharp using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; // Handling APIException try { var history = await client.GetTransactionHistoryAsync("bad-id", "com.example.myapp"); } catch (APIException ex) { // ex.HttpStatusCode — HTTP status (e.g. 404) // ex.ApiError — Nullable typed enum (e.g. APIError.TransactionIdNotFound) // ex.RawApiError — Raw long from Apple response (useful for unknown/future codes) // ex.ErrorMessage — Human-readable string from Apple Console.Error.WriteLine($"HTTP {ex.HttpStatusCode}: {ex.ApiError} — {ex.ErrorMessage}"); } // Handling VerificationException try { var decoded = await verifier.VerifyAndDecodeTransactionAsync("bad.jws.token"); } catch (VerificationException ex) { switch (ex.Status) { case VerificationStatus.RetryableVerificationFailure: // OCSP/network issue — retry after a delay break; case VerificationStatus.InvalidEnvironment: Console.Error.WriteLine("Environment mismatch (sandbox vs production)"); break; case VerificationStatus.InvalidBundleId: Console.Error.WriteLine("Bundle ID in payload does not match expected value"); break; default: Console.Error.WriteLine($"Verification failed: {ex.Status}"); break; } } ``` ``` -------------------------------- ### PromotionalOfferV2SignatureCreator Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Generates a JWS for the newer StoreKit 2 promotional offer API. ```APIDOC ## PromotionalOfferV2SignatureCreator — Create a promotional offer V2 JWS signature Generates a JWS for the newer StoreKit 2 promotional offer API. ```csharp using Enjna.AppStoreServerLibrary; var creator = new PromotionalOfferV2SignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55" ); var jws = creator.CreateSignature( productId: "com.example.myapp.subscription.annual", offerIdentifier: "promo-50off", bundleId: "com.example.myapp", transactionId: "1000000123456789" // optional — recommended for personalization ); Console.WriteLine($"JWS: {jws}"); // Pass this JWS to StoreKit's PromotionalOffer on the device ``` ``` -------------------------------- ### Mass Extend Renewal Date For All Active Subscribers Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Initiates a mass renewal date extension for all active subscribers of a given product. Requires product ID, extension details, and storefront country codes. The returned 'requestIdentifier' can be used to poll for status. ```csharp var massRequest = new MassExtendRenewalDateRequest { ProductId = "com.example.myapp.subscription.monthly", ExtendByDays = 3, ExtendReasonCode = ExtendReasonCode.ServiceIssueOrOutage, RequestIdentifier = "a1b2c3d4-e5f6-7890-abcd-ef1234567890", StorefrontCountryCodes = ["USA", "CAN", "GBR"] }; var massResponse = await client.ExtendRenewalDateForAllActiveSubscribersAsync( request: massRequest, bundleId: "com.example.myapp" ); Console.WriteLine($"Mass extension request ID: {massResponse.RequestIdentifier}"); // Poll for status var status = await client.GetStatusOfSubscriptionRenewalDateExtensionsAsync( requestIdentifier: massResponse.RequestIdentifier!, productId: massRequest.ProductId, bundleId: "com.example.myapp" ); Console.WriteLine($"Complete: {status.Complete}, Succeeded: {status.SucceededCount}, Failed: {status.FailedCount}"); ``` -------------------------------- ### Register SignedDataVerifier as Singleton Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Register the SignedDataVerifier service as a singleton. This service does not rely on HttpClient and can be directly instantiated. ```csharp builder.Services.AddSingleton(new SignedDataVerifier( appleRootCertificates: new[] { File.ReadAllBytes("/path/to/AppleRootCA-G3.cer") }, enableOnlineChecks: true, environment: AppStoreEnvironment.Production )); ``` -------------------------------- ### Handle API and Verification Exceptions Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Catch APIException for non-2xx HTTP responses from AppStoreServerAPIClient methods, and VerificationException for cryptographic or validation failures from SignedDataVerifier. Inspect exception properties for details. ```csharp using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; // Handling APIException try { var history = await client.GetTransactionHistoryAsync("bad-id", "com.example.myapp"); } catch (APIException ex) { // ex.HttpStatusCode — HTTP status (e.g. 404) // ex.ApiError — Nullable typed enum (e.g. APIError.TransactionIdNotFound) // ex.RawApiError — Raw long from Apple response (useful for unknown/future codes) // ex.ErrorMessage — Human-readable string from Apple Console.Error.WriteLine($"HTTP {ex.HttpStatusCode}: {ex.ApiError} — {ex.ErrorMessage}"); } // Handling VerificationException try { var decoded = await verifier.VerifyAndDecodeTransactionAsync("bad.jws.token"); } catch (VerificationException ex) { switch (ex.Status) { case VerificationStatus.RetryableVerificationFailure: // OCSP/network issue — retry after a delay break; case VerificationStatus.InvalidEnvironment: Console.Error.WriteLine("Environment mismatch (sandbox vs production)"); break; case VerificationStatus.InvalidBundleId: Console.Error.WriteLine("Bundle ID in payload does not match expected value"); break; default: Console.Error.WriteLine($"Verification failed: {ex.Status}"); break; } } ``` -------------------------------- ### Create Promotional Offer V2 JWS Signature Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Generates a JWS for the newer StoreKit 2 promotional offer API. Requires a private key, key ID, and issuer ID. ```csharp using Enjna.AppStoreServerLibrary; var creator = new PromotionalOfferV2SignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55" ); var jws = creator.CreateSignature( productId: "com.example.myapp.subscription.annual", offerIdentifier: "promo-50off", bundleId: "com.example.myapp", transactionId: "1000000123456789" // optional — recommended for personalization ); Console.WriteLine($"JWS: {jws}"); // Pass this JWS to StoreKit's PromotionalOffer on the device ``` -------------------------------- ### Manage Retention Messages and Images Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Utilize the AppStoreServerAPIClient to upload images and messages, configure default or real-time messages, and manage sandbox performance tests. Ensure UUIDs are lowercase before uploading. ```csharp // 1. Upload an image (UUID must be lowercase) var imageId = Guid.NewGuid().ToString().ToLower(); var imageBytes = File.ReadAllBytes("/path/to/banner.png"); await client.UploadImageAsync(imageId, imageBytes, "com.example.myapp", ImageSize.Large); // 2. Upload a message (UUID must be lowercase) var messageId = Guid.NewGuid().ToString().ToLower(); await client.UploadMessageAsync( messageIdentifier: messageId, request: new UploadMessageRequestBody { Title = "Don't go!", Body = "Stay and enjoy 3 months free.", ImageIdentifier = imageId }, bundleId: "com.example.myapp" ); // 3. Set as default for a product/locale await client.ConfigureDefaultMessageAsync( productId: "com.example.myapp.subscription.monthly", locale: "en-US", request: new DefaultConfigurationRequest { MessageIdentifier = messageId }, bundleId: "com.example.myapp" ); // 4. Configure real-time endpoint URL await client.ConfigureRealtimeUrlAsync( request: new RealtimeUrlRequest { Url = "https://api.example.com/retention-message" }, bundleId: "com.example.myapp" ); // 5. List all uploaded images var imageList = await client.GetImageListAsync("com.example.myapp"); foreach (var img in imageList.Images ?? []) Console.WriteLine($"Image {img.ImageIdentifier}: {img.ImageState}"); // 6. Initiate a sandbox performance test var perfTest = await client.InitiatePerformanceTestAsync( request: new PerformanceTestRequest { TransactionId = "1000000123456789" }, bundleId: "com.example.myapp" ); // 7. Poll for performance test results var results = await client.GetPerformanceTestResultsAsync( requestId: perfTest.RequestId!, bundleId: "com.example.myapp" ); Console.WriteLine($"Test status: {results.Status}, avg response: {results.ResponseTimes?.Average}ms"); ``` -------------------------------- ### Register AppStoreServerAPIClient with Named HttpClient Source: https://github.com/ahmedisam99/app-store-server-library-dotnet/blob/main/docfx/index.md Register the AppStoreServerAPIClient as a singleton using a named HttpClient. This approach ensures proper HttpClient lifetime management by the IHttpClientFactory. ```csharp builder.Services.AddHttpClient("AppStoreServer"); builder.Services.AddSingleton(sp => { var privateKey = File.ReadAllText("/path/to/key.p8"); var httpClient = sp.GetRequiredService().CreateClient("AppStoreServer"); return new AppStoreServerAPIClient( privateKey, keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55", environment: AppStoreEnvironment.Production, httpClient: httpClient ); }); ``` -------------------------------- ### PromotionalOfferSignatureCreator Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Generates a Base64-encoded ECDSA-SHA256 signature for presenting a promotional subscription offer via the original StoreKit API. ```APIDOC ## PromotionalOfferSignatureCreator — Create a promotional offer signature (original StoreKit API) Generates a Base64-encoded ECDSA-SHA256 signature for presenting a promotional subscription offer via the original StoreKit API. ```csharp using Enjna.AppStoreServerLibrary; using var signatureCreator = new PromotionalOfferSignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ" ); var nonce = Guid.NewGuid(); var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var signature = signatureCreator.CreateSignature( productIdentifier: "com.example.myapp.subscription.annual", subscriptionOfferId: "promo-50off", appAccountToken: "6a72b3fd-4b7c-4e60-9e4d-b9a1c2d3e4f5", nonce: nonce, timestamp: timestamp, bundleId: "com.example.myapp" ); // Return { keyIdentifier, nonce, timestamp, signature } to the client for StoreKit Console.WriteLine($"Signature: {signature}"); ``` ``` -------------------------------- ### GetRefundHistoryAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Returns all refunded in-app purchases for a customer, paginated using a revision token. ```APIDOC ## GetRefundHistoryAsync — Paginate refund history Returns all refunded in-app purchases for a customer, paginated via a revision token. ```csharp RefundHistoryResponse? refundResponse = null; var refundedSignedTransactions = new List(); do { refundResponse = await client.GetRefundHistoryAsync( anyTransactionId: "1000000123456789", bundleId: "com.example.myapp", revision: refundResponse?.Revision ); if (refundResponse.SignedTransactions is not null) refundedSignedTransactions.AddRange(refundResponse.SignedTransactions); } while (refundResponse.HasMore); Console.WriteLine($"Customer has {refundedSignedTransactions.Count} refunded transaction(s)"); ``` ``` -------------------------------- ### Look Up Order ID Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Resolves an App Store order ID to signed transaction payloads. Outputs the status of the order lookup and prints the first 40 characters of each signed transaction. ```csharp var orderLookup = await client.LookUpOrderIdAsync( orderId: "MQ1ABC2DEF", bundleId: "com.example.myapp" ); Console.WriteLine($"Order status: {orderLookup.Status}"); // Valid or Invalid foreach (var signedTx in orderLookup.SignedTransactions ?? []) { Console.WriteLine($"Signed transaction: {signedTx[..40]}..."); } ``` -------------------------------- ### Paginate Customer Transaction History (V2) Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Fetches a customer's transaction history using the V2 endpoint. Iterate through paginated results until 'HasMore' is false. Handles potential API exceptions. ```csharp using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; var request = new TransactionHistoryRequest { Sort = SortOrder.Ascending, Revoked = false, ProductTypes = [ProductType.AutoRenewable], StartDate = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeMilliseconds() }; var transactions = new List(); HistoryResponse? response = null; try { do { request.Revision = response?.Revision; response = await client.GetTransactionHistoryAsync( anyTransactionId: "1000000123456789", bundleId: "com.example.myapp", request: request, version: GetTransactionHistoryVersion.V2 ); if (response.SignedTransactions is not null) transactions.AddRange(response.SignedTransactions); } while (response.HasMore); Console.WriteLine($"Fetched {transactions.Count} signed transactions"); // Each entry in `transactions` is a JWS string — pass to SignedDataVerifier to decode } catch (APIException ex) { Console.Error.WriteLine($"HTTP {ex.HttpStatusCode}, error {ex.ApiError}: {ex.ErrorMessage}"); } ``` -------------------------------- ### SendConsumptionInformationAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Report consumption data to influence a potential refund decision when the App Store sends a CONSUMPTION_REQUEST server notification. ```APIDOC ## SendConsumptionInformationAsync ### Description Report consumption data to influence a potential refund decision when the App Store sends a `CONSUMPTION_REQUEST` server notification. ### Method ```csharp await client.SendConsumptionInformationAsync( transactionId: "1000000123456789", request: new ConsumptionRequest { CustomerConsented = true, ConsumptionPercentage = 75000, // 75.000% in milliunits DeliveryStatus = DeliveryStatus.DeliveredAndWorking, SampleContentProvided = false, RefundPreference = RefundPreference.NoPreference }, bundleId: "com.example.myapp" ); ``` ``` -------------------------------- ### ExtendRenewalDateForAllActiveSubscribersAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Initiates a mass renewal date extension for all active subscribers of a product. ```APIDOC ## ExtendRenewalDateForAllActiveSubscribersAsync — Mass renewal extension Kicks off a mass renewal-date extension for all active subscribers of a product. Poll the returned `requestIdentifier` with `GetStatusOfSubscriptionRenewalDateExtensionsAsync`. ```csharp var massRequest = new MassExtendRenewalDateRequest { ProductId = "com.example.myapp.subscription.monthly", ExtendByDays = 3, ExtendReasonCode = ExtendReasonCode.ServiceIssueOrOutage, RequestIdentifier = "a1b2c3d4-e5f6-7890-abcd-ef1234567890", StorefrontCountryCodes = ["USA", "CAN", "GBR"] }; var massResponse = await client.ExtendRenewalDateForAllActiveSubscribersAsync( request: massRequest, bundleId: "com.example.myapp" ); Console.WriteLine($"Mass extension request ID: {massResponse.RequestIdentifier}"); // Poll for status var status = await client.GetStatusOfSubscriptionRenewalDateExtensionsAsync( requestIdentifier: massResponse.RequestIdentifier!, productId: massRequest.ProductId, bundleId: "com.example.myapp" ); Console.WriteLine($"Complete: {status.Complete}, Succeeded: {status.SucceededCount}, Failed: {status.FailedCount}"); ``` ``` -------------------------------- ### Retention Messaging API Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt The AppStoreServerAPIClient exposes the full Retention Messaging API for uploading promotional content and configuring real-time or default messages shown to at-risk subscribers. ```APIDOC ## Retention Messaging API — Upload, configure, and manage messages and images The `AppStoreServerAPIClient` exposes the full Retention Messaging API for uploading promotional content and configuring real-time or default messages shown to at-risk subscribers. ```csharp // 1. Upload an image (UUID must be lowercase) var imageId = Guid.NewGuid().ToString().ToLower(); var imageBytes = File.ReadAllBytes("/path/to/banner.png"); await client.UploadImageAsync(imageId, imageBytes, "com.example.myapp", ImageSize.Large); // 2. Upload a message (UUID must be lowercase) var messageId = Guid.NewGuid().ToString().ToLower(); await client.UploadMessageAsync( messageIdentifier: messageId, request: new UploadMessageRequestBody { Title = "Don't go!", Body = "Stay and enjoy 3 months free.", ImageIdentifier = imageId }, bundleId: "com.example.myapp" ); // 3. Set as default for a product/locale await client.ConfigureDefaultMessageAsync( productId: "com.example.myapp.subscription.monthly", locale: "en-US", request: new DefaultConfigurationRequest { MessageIdentifier = messageId }, bundleId: "com.example.myapp" ); // 4. Configure real-time endpoint URL await client.ConfigureRealtimeUrlAsync( request: new RealtimeUrlRequest { Url = "https://api.example.com/retention-message" }, bundleId: "com.example.myapp" ); // 5. List all uploaded images var imageList = await client.GetImageListAsync("com.example.myapp"); foreach (var img in imageList.Images ?? []) Console.WriteLine($"Image {img.ImageIdentifier}: {img.ImageState}"); // 6. Initiate a sandbox performance test var perfTest = await client.InitiatePerformanceTestAsync( request: new PerformanceTestRequest { TransactionId = "1000000123456789" }, bundleId: "com.example.myapp" ); // 7. Poll for performance test results var results = await client.GetPerformanceTestResultsAsync( requestId: perfTest.RequestId!, bundleId: "com.example.myapp" ); Console.WriteLine($"Test status: {results.Status}, avg response: {results.ResponseTimes?.Average}ms"); ``` ``` -------------------------------- ### HelperValidationUtils Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Static validation helpers for Advanced Commerce field constraints before building a request. These methods check for nulls, length limits, and valid ranges. ```APIDOC ## HelperValidationUtils — Validate Advanced Commerce request fields Static validation helpers for Advanced Commerce field constraints before building a request. ```csharp using Enjna.AppStoreServerLibrary; // Validate before constructing the request var description = "Pro subscription, unlimited access to all"; var displayName = "Pro Monthly"; var sku = "SKU-PRO-MONTHLY-2024"; var periodCount = 6; if (!HelperValidationUtils.ValidateDescription(description)) throw new ArgumentException($"Description exceeds {HelperValidationUtils.MaximumDescriptionLength} chars or is null"); if (!HelperValidationUtils.ValidateDisplayName(displayName)) throw new ArgumentException($"Display name exceeds {HelperValidationUtils.MaximumDisplayNameLength} chars or is null"); if (!HelperValidationUtils.ValidateSku(sku)) throw new ArgumentException($"SKU exceeds {HelperValidationUtils.MaximumSkuLength} chars or is null"); if (!HelperValidationUtils.ValidatePeriodCount(periodCount)) throw new ArgumentException($"Period count must be between {HelperValidationUtils.MinPeriodCount} and {HelperValidationUtils.MaxPeriodCount}"); Console.WriteLine("All fields valid — safe to build request"); ``` ``` -------------------------------- ### GetAllSubscriptionStatusesAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Retrieves all auto-renewable subscription statuses for a customer, with optional filtering by status. ```APIDOC ## GetAllSubscriptionStatusesAsync — Get active subscription statuses Returns all auto-renewable subscription statuses for a customer, optionally filtered to specific `Status` values (active, expired, billing retry, etc.). ```csharp using Enjna.AppStoreServerLibrary.Models.Enums; try { var statusResponse = await client.GetAllSubscriptionStatusesAsync( anyTransactionId: "1000000123456789", bundleId: "com.example.myapp", status: [Status.Active, Status.BillingRetry] ); foreach (var group in statusResponse.Data ?? []) { Console.WriteLine($"Subscription group: {group.SubscriptionGroupIdentifier}"); foreach (var item in group.LastTransactions ?? []) { Console.WriteLine($" originalTxId={item.OriginalTransactionId}, status={item.Status}"); // item.SignedTransactionInfo and item.SignedRenewalInfo are JWS strings for verification } } } catch (APIException ex) when (ex.ApiError == APIError.AccountNotFound) { Console.Error.WriteLine("Customer account not found in App Store"); } ``` ``` -------------------------------- ### RequestTestNotificationAsync / GetTestNotificationStatusAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Triggers a test App Store Server Notification and then polls its delivery status. ```APIDOC ## RequestTestNotificationAsync / GetTestNotificationStatusAsync ### Description Triggers a test App Store Server Notification and then polls its delivery status. ### Method ```csharp // Trigger a test notification var testResponse = await client.RequestTestNotificationAsync("com.example.myapp"); Console.WriteLine($"Test token: {testResponse.TestNotificationToken}"); // Poll for delivery result var checkResponse = await client.GetTestNotificationStatusAsync( testNotificationToken: testResponse.TestNotificationToken!, bundleId: "com.example.myapp" ); foreach (var attempt in checkResponse.SendAttempts ?? []) { Console.WriteLine($"Attempt at {attempt.AttemptDate}: {attempt.SendAttemptResult}"); } ``` ``` -------------------------------- ### SignedDataVerifier.VerifyAndDecodeAppTransactionAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Verifies and decodes a signed AppTransaction, which contains the app-level purchase record. This is essential for understanding the overall purchase history of an app. ```APIDOC ## SignedDataVerifier.VerifyAndDecodeAppTransactionAsync — Decode an app transaction ### Description Verifies and decodes a signed AppTransaction (contains the app-level purchase record). ### Method `VerifyAndDecodeAppTransactionAsync` ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None ### Request Example ```csharp var appTx = await verifier.VerifyAndDecodeAppTransactionAsync( signedAppTransaction: "eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0...", bundleId: "com.example.myapp", appAppleId: 1234567890L ); ``` ### Response #### Success Response Returns an `AppTransaction` object with decoded app transaction details. #### Response Example ```csharp Console.WriteLine($"bundleId : {appTx.BundleId}"); Console.WriteLine($"appVersion : {appTx.ApplicationVersion}"); ``` ``` -------------------------------- ### GetTransactionHistoryAsync Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Fetches the full paginated transaction history for a customer. This method allows filtering by product type, date range, sort order, or ownership type. Version 2 is recommended, and iteration continues until `HasMore` is false. ```APIDOC ## GetTransactionHistoryAsync — Paginate a customer's transaction history Fetches the full paginated transaction history for a customer, filtered by product type, date range, sort order, or ownership type. V2 is recommended. Iterate until `HasMore` is false. ```csharp using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; var request = new TransactionHistoryRequest { Sort = SortOrder.Ascending, Revoked = false, ProductTypes = [ProductType.AutoRenewable], StartDate = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeMilliseconds() }; var transactions = new List(); HistoryResponse? response = null; try { do { request.Revision = response?.Revision; response = await client.GetTransactionHistoryAsync( anyTransactionId: "1000000123456789", bundleId: "com.example.myapp", request: request, version: GetTransactionHistoryVersion.V2 ); if (response.SignedTransactions is not null) transactions.AddRange(response.SignedTransactions); } while (response.HasMore); Console.WriteLine($"Fetched {transactions.Count} signed transactions"); // Each entry in `transactions` is a JWS string — pass to SignedDataVerifier to decode } catch (APIException ex) { Console.Error.WriteLine($"HTTP {ex.HttpStatusCode}, error {ex.ApiError}: {ex.ErrorMessage}"); } ``` ``` -------------------------------- ### AdvancedCommerceInAppSignatureCreator Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Produces a JWS for the Advanced Commerce API, encoding any request object as a Base64 JSON payload. ```APIDOC ## AdvancedCommerceInAppSignatureCreator — Sign an Advanced Commerce in-app request Produces a JWS for the Advanced Commerce API, encoding any request object as a Base64 JSON payload. ```csharp using Enjna.AppStoreServerLibrary; using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; var acCreator = new AdvancedCommerceInAppSignatureCreator( signingKey: File.ReadAllText("/path/to/AuthKey_ABCDEFGHIJ.p8"), keyId: "ABCDEFGHIJ", issuerId: "99b16628-15e4-4668-972b-eeff55eeff55" ); var subscriptionRequest = new AdvancedCommerceSubscriptionCreateRequest { Currency = "USD", TaxCode = "APD_electronic_software", Period = new AdvancedCommercePeriod { Count = 1, Unit = AdvancedCommercePeriodUnit.Month }, Descriptors = new AdvancedCommerceDescriptors { DisplayName = "Pro Monthly", Description = "Unlimited access" }, Items = [ new AdvancedCommerceSubscriptionCreateItem { Sku = "SKU-PRO-MONTHLY", Amount = 999, // in cents DisplayName = "Pro" } ] }; var jws = acCreator.CreateSignature(subscriptionRequest, "com.example.myapp"); // Pass this JWS to StoreKit's Product.PurchaseOption.customData on the device Console.WriteLine($"Advanced Commerce JWS: {jws}"); ``` ``` -------------------------------- ### Extend Subscription Renewal Date Async Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Extends the renewal date for a single active subscription. Requires specifying the extension duration, reason code, and a unique request identifier. Useful for service disruptions. ```csharp using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; var extendRequest = new ExtendRenewalDateRequest { ExtendByDays = 7, ExtendReasonCode = ExtendReasonCode.CustomerSatisfaction, RequestIdentifier = Guid.NewGuid().ToString() }; var extendResponse = await client.ExtendSubscriptionRenewalDateAsync( originalTransactionId: "1000000123456789", request: extendRequest, bundleId: "com.example.myapp" ); Console.WriteLine($"Extension succeeded: {extendResponse.Success}"); Console.WriteLine($"New effective date: {extendResponse.EffectiveDate}"); ``` -------------------------------- ### SignedDataVerifier Source: https://context7.com/ahmedisam99/app-store-server-library-dotnet/llms.txt Construct and verify JWS payloads from Apple, including transactions, renewal info, server notifications, app transactions, and real-time retention requests. ```APIDOC ## SignedDataVerifier ### Description Verifies and decodes any JWS payload from Apple (transactions, renewal info, server notifications, app transactions, real-time retention requests). Pass DER-encoded Apple root CAs downloaded from [Apple PKI](https://www.apple.com/certificateauthority/). ### Method ```csharp using Enjna.AppStoreServerLibrary; using Enjna.AppStoreServerLibrary.Models; using Enjna.AppStoreServerLibrary.Models.Enums; var verifier = new SignedDataVerifier( appleRootCertificates: new[] { File.ReadAllBytes("/path/to/AppleRootCA-G3.cer"), File.ReadAllBytes("/path/to/AppleRootCA-G2.cer") }, enableOnlineChecks: true, // enables OCSP revocation + current-time expiration check environment: AppStoreEnvironment.Production ); // For DI (thread-safe singleton, implements IDisposable): builder.Services.AddSingleton(verifier); ``` ```