From c649a65577ee924b880267552d0993912f2e5e2f Mon Sep 17 00:00:00 2001 From: Andrii Date: Tue, 3 Feb 2026 14:18:54 +0200 Subject: [PATCH] Implement GetCallbacksBySubscriptionIdAsync - webhook callback event history related to subscription ID with advanced filtering and sorting capabilities --- CHANGELOG.md | 1 + .../GetCallbacksBySubscriptionIdExample.cs | 76 +++++++++++++ ...GetCallbacksBySubscriptionIdServiceTest.cs | 52 +++++++++ .../Services/EventSubscriptionServiceTest.cs | 100 +++++++++++++++++- .../Interfaces/IEventSubscriptionService.cs | 11 ++ .../Service/EventSubscriptionService.cs | 21 ++++ 6 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs create mode 100644 SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c48bc1..e8d9c9bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org). - Get Event Subscription by ID: Retrieve detailed information about specific event subscriptions - Delete Event Subscription: Remove event subscriptions programmatically - Get Callbacks History: Comprehensive webhook callback event history with advanced filtering and sorting capabilities + - Get Callbacks History by ID: webhook callback event history related to subscription ID with advanced filtering and sorting capabilities - Document Template Operations: - Template Routing Management: Create, retrieve, and update routing configurations for document templates - Bulk Invite from Template: Send signing invitations to multiple recipients using templates diff --git a/SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs b/SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs new file mode 100644 index 00000000..90375efb --- /dev/null +++ b/SignNow.Net.Examples/Webhooks/GetCallbacksBySubscriptionIdExample.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SignNow.Net.Model.Requests; +using SignNow.Net.Model.Requests.GetFolderQuery; + +namespace SignNow.Net.Examples +{ + [TestClass] + public class GetCallbacksBySubscriptionIdExample : ExamplesBase + { + /// + /// Demonstrates how to get webhook callback events for a specific event subscription. + /// This example shows how to retrieve callback history filtered by subscription ID and analyze webhook delivery results. + /// + /// + [TestMethod] + public async Task GetCallbacksBySubscriptionIdAsync() + { + var subscriptions = await testContext.Events + .GetEventSubscriptionsListAsync() + .ConfigureAwait(false); + + if (subscriptions?.Data == null || subscriptions.Data.Count == 0) + { + Console.WriteLine("No event subscriptions found. Please create an event subscription first."); + return; + } + + var subscriptionId = subscriptions.Data.First().Id; + Console.WriteLine($"Using subscription ID: {subscriptionId}"); + + // Example 1: Get all callbacks for a specific subscription with default options + Console.WriteLine("\n=== Example 1: Get all callbacks for specific subscription ==="); + var allCallbacks = await testContext.Events + .GetCallbacksBySubscriptionIdAsync(subscriptionId) + .ConfigureAwait(false); + + Console.WriteLine($"Total callbacks found for subscription {subscriptionId}: {allCallbacks.Data.Count}"); + Console.WriteLine($"Current page: {allCallbacks.Meta.Pagination.CurrentPage}"); + Console.WriteLine($"Per page: {allCallbacks.Meta.Pagination.PerPage}"); + Console.WriteLine($"Total pages: {allCallbacks.Meta.Pagination.TotalPages}"); + Console.WriteLine($"Total items: {allCallbacks.Meta.Pagination.Total}"); + + // Example 2: Filter successful callbacks for the subscription + Console.WriteLine($"\n=== Example 2: Get successful callbacks for subscription {subscriptionId} ==="); + var successfulCallbacks = await testContext.Events + .GetCallbacksBySubscriptionIdAsync(subscriptionId, new GetCallbacksOptions + { + Filters = f => f.Or( + f => f.Date.Between(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow), + f => f.Code.Between(200, 299), + f => f.CallbackUrl.Like("example") + ), + Sortings = s => s.Code(SortOrder.Descending), + PerPage = 10 + }) + .ConfigureAwait(false); + + Console.WriteLine($"Successful callbacks: {successfulCallbacks.Data.Count}"); + foreach (var callback in successfulCallbacks.Data.Take(3)) + { + Console.WriteLine($" ID: {callback.Id}"); + Console.WriteLine($" Subscription ID: {callback.EventSubscriptionId}"); + Console.WriteLine($" Status Code: {callback.ResponseStatusCode}"); + Console.WriteLine($" Event: {callback.EventName}"); + Console.WriteLine($" Entity ID: {callback.EntityId}"); + Console.WriteLine($" Duration: {callback.Duration:F3}s"); + Console.WriteLine($" Start Time: {callback.RequestStartTime}"); + Console.WriteLine($" Callback Url: {callback.CallbackUrl}"); + Console.WriteLine(); + } + } + } +} diff --git a/SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs b/SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs new file mode 100644 index 00000000..365a3f4f --- /dev/null +++ b/SignNow.Net.Test/AcceptanceTests/GetCallbacksBySubscriptionIdServiceTest.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SignNow.Net.Model.Requests; +using SignNow.Net.Model.Requests.GetFolderQuery; +using UnitTests; + +namespace AcceptanceTests +{ + [TestClass] + public class GetCallbacksBySubscriptionIdServiceTest : AuthorizedApiTestBase + { + [TestMethod] + public async Task GetCallbacksBySubscriptionIdAsync_WithFilters_ReturnsFilteredResults() + { + var eventSubscriptions = await SignNowTestContext.Events + .GetEventSubscriptionsListAsync() + .ConfigureAwait(false); + + if (eventSubscriptions?.Data == null || eventSubscriptions.Data.Count == 0) + { + Assert.Inconclusive("No event subscriptions available for testing"); + return; + } + + var subscriptionId = eventSubscriptions.Data[0].Id; + + var options = new GetCallbacksOptions + { + Filters = f => f.Code.Between(200, 599), + Sortings = s => s.StartTime(SortOrder.Descending), + PerPage = 15 + }; + + var response = await SignNowTestContext.Events + .GetCallbacksBySubscriptionIdAsync(subscriptionId, options) + .ConfigureAwait(false); + + Assert.IsNotNull(response); + Assert.IsNotNull(response.Data); + Assert.IsNotNull(response.Meta); + Assert.AreEqual(15, response.Meta.Pagination.PerPage); + + // Verify all callbacks belong to the specified subscription and match filter criteria + foreach (var callback in response.Data) + { + Assert.AreEqual(subscriptionId, callback.EventSubscriptionId); + Assert.IsTrue(callback.ResponseStatusCode >= 200 && callback.ResponseStatusCode <= 299, + $"Callback status code {callback.ResponseStatusCode} should be in range 200-299"); + } + } + } +} diff --git a/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs b/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs index fb56fe51..3b9194b5 100644 --- a/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs +++ b/SignNow.Net.Test/UnitTests/Services/EventSubscriptionServiceTest.cs @@ -386,4 +386,102 @@ public async Task GetCallbacksAsync_WithNullRequestHeaders_ShouldHandleGracefull Assert.IsNotNull(callback.RequestContent); Assert.IsNotNull(callback.RequestContent.Content); } - }} + + [TestMethod] + public async Task GetCallbacksBySubscriptionIdAsync_WithValidSubscriptionId_ShouldReturnCallbacks() + { + var subscriptionId = "8a49e32e267e42a18e4a3967669f347e06e3b71e"; + var mockResponse = TestUtils.SerializeToJsonFormatted(new + { + data = new[] + { + new + { + id = "callback_123", + application_name = "TestApp", + entity_id = "doc_456", + event_subscription_id = subscriptionId, + event_subscription_active = true, + entity_type = "document", + event_name = "document.complete", + callback_url = "https://example.com/webhook", + request_method = "POST", + duration = 1.5, + request_start_time = 1609459200, + request_end_time = 1609459205, + request_headers = new + { + string_head = "test_value", + int_head = 42, + bool_head = true, + float_head = 3.14f + }, + response_content = "OK", + response_status_code = 200, + event_subscription_owner_email = "owner@example.com", + request_content = new + { + meta = new + { + timestamp = 1609459200, + @event = "document.complete", + environment = "https://api.signnow.com/", + initiator_id = "user_789", + callback_url = "https://example.com/webhook", + access_token = "***masked***" + }, + content = new + { + document_id = "doc_456", + document_name = "Test Document.pdf", + user_id = "user_789", + initiator_id = "user_789", + initiator_email = "initiator@example.com" + } + } + } + }, + meta = new + { + pagination = new + { + total = 1, + count = 1, + per_page = 50, + current_page = 1, + total_pages = 1 + } + } + }); + + var service = new EventSubscriptionService(ApiBaseUrl, new Token(), SignNowClientMock(mockResponse)); + var options = new GetCallbacksOptions + { + Page = 1, + PerPage = 50 + }; + + var response = await service.GetCallbacksBySubscriptionIdAsync(subscriptionId, options).ConfigureAwait(false); + + Assert.IsNotNull(response); + Assert.IsNotNull(response.Data); + Assert.IsNotNull(response.Meta); + Assert.AreEqual(1, response.Data.Count); + + var callback = response.Data[0]; + Assert.AreEqual("callback_123", callback.Id); + Assert.AreEqual("TestApp", callback.ApplicationName); + Assert.AreEqual("doc_456", callback.EntityId); + Assert.AreEqual(subscriptionId, callback.EventSubscriptionId); + Assert.IsTrue(callback.EventSubscriptionActive); + Assert.AreEqual(EventSubscriptionEntityType.Document, callback.EntityType); + Assert.AreEqual(EventType.DocumentComplete, callback.EventName); + Assert.AreEqual("https://example.com/webhook", callback.CallbackUrl.ToString()); + Assert.AreEqual(200, callback.ResponseStatusCode); + + Assert.AreEqual(1, response.Meta.Pagination.Total); + Assert.AreEqual(1, response.Meta.Pagination.Count); + Assert.AreEqual(50, response.Meta.Pagination.PerPage); + } + } +} diff --git a/SignNow.Net/Interfaces/IEventSubscriptionService.cs b/SignNow.Net/Interfaces/IEventSubscriptionService.cs index 37cd1fc1..4533f1c1 100644 --- a/SignNow.Net/Interfaces/IEventSubscriptionService.cs +++ b/SignNow.Net/Interfaces/IEventSubscriptionService.cs @@ -97,5 +97,16 @@ public interface IEventSubscriptionService /// Propagates notification that operations should be canceled. /// List of callback events with metadata Task GetCallbacksAsync(GetCallbacksOptions options = default, CancellationToken cancellationToken = default); + + /// + /// Allows users to get the list of webhook events (events history) by the subscription ID. + /// The results can be filtered and sorted. If the sort parameter is not indicated, + /// the results are sorted by the start_time in descending order. + /// + /// ID of the subscription + /// Options for filtering and sorting callbacks + /// Propagates notification that operations should be canceled. + /// List of callback events for the specified subscription with metadata + Task GetCallbacksBySubscriptionIdAsync(string subscriptionId, GetCallbacksOptions options = default, CancellationToken cancellationToken = default); } } diff --git a/SignNow.Net/Service/EventSubscriptionService.cs b/SignNow.Net/Service/EventSubscriptionService.cs index f793ddd6..7668ff2b 100644 --- a/SignNow.Net/Service/EventSubscriptionService.cs +++ b/SignNow.Net/Service/EventSubscriptionService.cs @@ -197,5 +197,26 @@ public async Task GetCallbacksAsync(GetCallbacksOptions optio .RequestAsync(requestOptions, cancellationToken) .ConfigureAwait(false); } + + /// + public async Task GetCallbacksBySubscriptionIdAsync(string subscriptionId, GetCallbacksOptions options = default, CancellationToken cancellationToken = default) + { + Token.TokenType = TokenType.Bearer; + + var query = options?.ToQueryString(); + var filters = string.IsNullOrEmpty(query) + ? string.Empty + : $"?{query}"; + + var requestOptions = new GetHttpRequestOptions + { + RequestUrl = new Uri(ApiBaseUrl, $"/v2/event-subscriptions/{subscriptionId.ValidateId()}/callbacks{filters}"), + Token = Token + }; + + return await SignNowClient + .RequestAsync(requestOptions, cancellationToken) + .ConfigureAwait(false); + } } }